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.
2136 lines
70 KiB
2136 lines
70 KiB
from sympy.core import Add, Mul, S
|
|
from sympy.core.containers import Tuple
|
|
from sympy.core.exprtools import factor_terms
|
|
from sympy.core.numbers import I
|
|
from sympy.core.relational import Eq, Equality
|
|
from sympy.core.sorting import default_sort_key, ordered
|
|
from sympy.core.symbol import Dummy, Symbol
|
|
from sympy.core.function import (expand_mul, expand, Derivative,
|
|
AppliedUndef, Function, Subs)
|
|
from sympy.functions import (exp, im, cos, sin, re, Piecewise,
|
|
piecewise_fold, sqrt, log)
|
|
from sympy.functions.combinatorial.factorials import factorial
|
|
from sympy.matrices import zeros, Matrix, NonSquareMatrixError, MatrixBase, eye
|
|
from sympy.polys import Poly, together
|
|
from sympy.simplify import collect, radsimp, signsimp # type: ignore
|
|
from sympy.simplify.powsimp import powdenest, powsimp
|
|
from sympy.simplify.ratsimp import ratsimp
|
|
from sympy.simplify.simplify import simplify
|
|
from sympy.sets.sets import FiniteSet
|
|
from sympy.solvers.deutils import ode_order
|
|
from sympy.solvers.solveset import NonlinearError, solveset
|
|
from sympy.utilities.iterables import (connected_components, iterable,
|
|
strongly_connected_components)
|
|
from sympy.utilities.misc import filldedent
|
|
from sympy.integrals.integrals import Integral, integrate
|
|
|
|
|
|
def _get_func_order(eqs, funcs):
|
|
return {func: max(ode_order(eq, func) for eq in eqs) for func in funcs}
|
|
|
|
|
|
class ODEOrderError(ValueError):
|
|
"""Raised by linear_ode_to_matrix if the system has the wrong order"""
|
|
pass
|
|
|
|
|
|
class ODENonlinearError(NonlinearError):
|
|
"""Raised by linear_ode_to_matrix if the system is nonlinear"""
|
|
pass
|
|
|
|
|
|
def _simpsol(soleq):
|
|
lhs = soleq.lhs
|
|
sol = soleq.rhs
|
|
sol = powsimp(sol)
|
|
gens = list(sol.atoms(exp))
|
|
p = Poly(sol, *gens, expand=False)
|
|
gens = [factor_terms(g) for g in gens]
|
|
if not gens:
|
|
gens = p.gens
|
|
syms = [Symbol('C1'), Symbol('C2')]
|
|
terms = []
|
|
for coeff, monom in zip(p.coeffs(), p.monoms()):
|
|
coeff = piecewise_fold(coeff)
|
|
if isinstance(coeff, Piecewise):
|
|
coeff = Piecewise(*((ratsimp(coef).collect(syms), cond) for coef, cond in coeff.args))
|
|
else:
|
|
coeff = ratsimp(coeff).collect(syms)
|
|
monom = Mul(*(g ** i for g, i in zip(gens, monom)))
|
|
terms.append(coeff * monom)
|
|
return Eq(lhs, Add(*terms))
|
|
|
|
|
|
def _solsimp(e, t):
|
|
no_t, has_t = powsimp(expand_mul(e)).as_independent(t)
|
|
|
|
no_t = ratsimp(no_t)
|
|
has_t = has_t.replace(exp, lambda a: exp(factor_terms(a)))
|
|
|
|
return no_t + has_t
|
|
|
|
|
|
def simpsol(sol, wrt1, wrt2, doit=True):
|
|
"""Simplify solutions from dsolve_system."""
|
|
|
|
# The parameter sol is the solution as returned by dsolve (list of Eq).
|
|
#
|
|
# The parameters wrt1 and wrt2 are lists of symbols to be collected for
|
|
# with those in wrt1 being collected for first. This allows for collecting
|
|
# on any factors involving the independent variable before collecting on
|
|
# the integration constants or vice versa using e.g.:
|
|
#
|
|
# sol = simpsol(sol, [t], [C1, C2]) # t first, constants after
|
|
# sol = simpsol(sol, [C1, C2], [t]) # constants first, t after
|
|
#
|
|
# If doit=True (default) then simpsol will begin by evaluating any
|
|
# unevaluated integrals. Since many integrals will appear multiple times
|
|
# in the solutions this is done intelligently by computing each integral
|
|
# only once.
|
|
#
|
|
# The strategy is to first perform simple cancellation with factor_terms
|
|
# and then multiply out all brackets with expand_mul. This gives an Add
|
|
# with many terms.
|
|
#
|
|
# We split each term into two multiplicative factors dep and coeff where
|
|
# all factors that involve wrt1 are in dep and any constant factors are in
|
|
# coeff e.g.
|
|
# sqrt(2)*C1*exp(t) -> ( exp(t), sqrt(2)*C1 )
|
|
#
|
|
# The dep factors are simplified using powsimp to combine expanded
|
|
# exponential factors e.g.
|
|
# exp(a*t)*exp(b*t) -> exp(t*(a+b))
|
|
#
|
|
# We then collect coefficients for all terms having the same (simplified)
|
|
# dep. The coefficients are then simplified using together and ratsimp and
|
|
# lastly by recursively applying the same transformation to the
|
|
# coefficients to collect on wrt2.
|
|
#
|
|
# Finally the result is recombined into an Add and signsimp is used to
|
|
# normalise any minus signs.
|
|
|
|
def simprhs(rhs, rep, wrt1, wrt2):
|
|
"""Simplify the rhs of an ODE solution"""
|
|
if rep:
|
|
rhs = rhs.subs(rep)
|
|
rhs = factor_terms(rhs)
|
|
rhs = simp_coeff_dep(rhs, wrt1, wrt2)
|
|
rhs = signsimp(rhs)
|
|
return rhs
|
|
|
|
def simp_coeff_dep(expr, wrt1, wrt2=None):
|
|
"""Split rhs into terms, split terms into dep and coeff and collect on dep"""
|
|
add_dep_terms = lambda e: e.is_Add and e.has(*wrt1)
|
|
expandable = lambda e: e.is_Mul and any(map(add_dep_terms, e.args))
|
|
expand_func = lambda e: expand_mul(e, deep=False)
|
|
expand_mul_mod = lambda e: e.replace(expandable, expand_func)
|
|
terms = Add.make_args(expand_mul_mod(expr))
|
|
dc = {}
|
|
for term in terms:
|
|
coeff, dep = term.as_independent(*wrt1, as_Add=False)
|
|
# Collect together the coefficients for terms that have the same
|
|
# dependence on wrt1 (after dep is normalised using simpdep).
|
|
dep = simpdep(dep, wrt1)
|
|
|
|
# See if the dependence on t cancels out...
|
|
if dep is not S.One:
|
|
dep2 = factor_terms(dep)
|
|
if not dep2.has(*wrt1):
|
|
coeff *= dep2
|
|
dep = S.One
|
|
|
|
if dep not in dc:
|
|
dc[dep] = coeff
|
|
else:
|
|
dc[dep] += coeff
|
|
# Apply the method recursively to the coefficients but this time
|
|
# collecting on wrt2 rather than wrt2.
|
|
termpairs = ((simpcoeff(c, wrt2), d) for d, c in dc.items())
|
|
if wrt2 is not None:
|
|
termpairs = ((simp_coeff_dep(c, wrt2), d) for c, d in termpairs)
|
|
return Add(*(c * d for c, d in termpairs))
|
|
|
|
def simpdep(term, wrt1):
|
|
"""Normalise factors involving t with powsimp and recombine exp"""
|
|
def canonicalise(a):
|
|
# Using factor_terms here isn't quite right because it leads to things
|
|
# like exp(t*(1+t)) that we don't want. We do want to cancel factors
|
|
# and pull out a common denominator but ideally the numerator would be
|
|
# expressed as a standard form polynomial in t so we expand_mul
|
|
# and collect afterwards.
|
|
a = factor_terms(a)
|
|
num, den = a.as_numer_denom()
|
|
num = expand_mul(num)
|
|
num = collect(num, wrt1)
|
|
return num / den
|
|
|
|
term = powsimp(term)
|
|
rep = {e: exp(canonicalise(e.args[0])) for e in term.atoms(exp)}
|
|
term = term.subs(rep)
|
|
return term
|
|
|
|
def simpcoeff(coeff, wrt2):
|
|
"""Bring to a common fraction and cancel with ratsimp"""
|
|
coeff = together(coeff)
|
|
if coeff.is_polynomial():
|
|
# Calling ratsimp can be expensive. The main reason is to simplify
|
|
# sums of terms with irrational denominators so we limit ourselves
|
|
# to the case where the expression is polynomial in any symbols.
|
|
# Maybe there's a better approach...
|
|
coeff = ratsimp(radsimp(coeff))
|
|
# collect on secondary variables first and any remaining symbols after
|
|
if wrt2 is not None:
|
|
syms = list(wrt2) + list(ordered(coeff.free_symbols - set(wrt2)))
|
|
else:
|
|
syms = list(ordered(coeff.free_symbols))
|
|
coeff = collect(coeff, syms)
|
|
coeff = together(coeff)
|
|
return coeff
|
|
|
|
# There are often repeated integrals. Collect unique integrals and
|
|
# evaluate each once and then substitute into the final result to replace
|
|
# all occurrences in each of the solution equations.
|
|
if doit:
|
|
integrals = set().union(*(s.atoms(Integral) for s in sol))
|
|
rep = {i: factor_terms(i).doit() for i in integrals}
|
|
else:
|
|
rep = {}
|
|
|
|
sol = [Eq(s.lhs, simprhs(s.rhs, rep, wrt1, wrt2)) for s in sol]
|
|
return sol
|
|
|
|
|
|
def linodesolve_type(A, t, b=None):
|
|
r"""
|
|
Helper function that determines the type of the system of ODEs for solving with :obj:`sympy.solvers.ode.systems.linodesolve()`
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function takes in the coefficient matrix and/or the non-homogeneous term
|
|
and returns the type of the equation that can be solved by :obj:`sympy.solvers.ode.systems.linodesolve()`.
|
|
|
|
If the system is constant coefficient homogeneous, then "type1" is returned
|
|
|
|
If the system is constant coefficient non-homogeneous, then "type2" is returned
|
|
|
|
If the system is non-constant coefficient homogeneous, then "type3" is returned
|
|
|
|
If the system is non-constant coefficient non-homogeneous, then "type4" is returned
|
|
|
|
If the system has a non-constant coefficient matrix which can be factorized into constant
|
|
coefficient matrix, then "type5" or "type6" is returned for when the system is homogeneous or
|
|
non-homogeneous respectively.
|
|
|
|
Note that, if the system of ODEs is of "type3" or "type4", then along with the type,
|
|
the commutative antiderivative of the coefficient matrix is also returned.
|
|
|
|
If the system cannot be solved by :obj:`sympy.solvers.ode.systems.linodesolve()`, then
|
|
NotImplementedError is raised.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A : Matrix
|
|
Coefficient matrix of the system of ODEs
|
|
b : Matrix or None
|
|
Non-homogeneous term of the system. The default value is None.
|
|
If this argument is None, then the system is assumed to be homogeneous.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import symbols, Matrix
|
|
>>> from sympy.solvers.ode.systems import linodesolve_type
|
|
>>> t = symbols("t")
|
|
>>> A = Matrix([[1, 1], [2, 3]])
|
|
>>> b = Matrix([t, 1])
|
|
|
|
>>> linodesolve_type(A, t)
|
|
{'antiderivative': None, 'type_of_equation': 'type1'}
|
|
|
|
>>> linodesolve_type(A, t, b=b)
|
|
{'antiderivative': None, 'type_of_equation': 'type2'}
|
|
|
|
>>> A_t = Matrix([[1, t], [-t, 1]])
|
|
|
|
>>> linodesolve_type(A_t, t)
|
|
{'antiderivative': Matrix([
|
|
[ t, t**2/2],
|
|
[-t**2/2, t]]), 'type_of_equation': 'type3'}
|
|
|
|
>>> linodesolve_type(A_t, t, b=b)
|
|
{'antiderivative': Matrix([
|
|
[ t, t**2/2],
|
|
[-t**2/2, t]]), 'type_of_equation': 'type4'}
|
|
|
|
>>> A_non_commutative = Matrix([[1, t], [t, -1]])
|
|
>>> linodesolve_type(A_non_commutative, t)
|
|
Traceback (most recent call last):
|
|
...
|
|
NotImplementedError:
|
|
The system does not have a commutative antiderivative, it cannot be
|
|
solved by linodesolve.
|
|
|
|
Returns
|
|
=======
|
|
|
|
Dict
|
|
|
|
Raises
|
|
======
|
|
|
|
NotImplementedError
|
|
When the coefficient matrix does not have a commutative antiderivative
|
|
|
|
See Also
|
|
========
|
|
|
|
linodesolve: Function for which linodesolve_type gets the information
|
|
|
|
"""
|
|
|
|
match = {}
|
|
is_non_constant = not _matrix_is_constant(A, t)
|
|
is_non_homogeneous = not (b is None or b.is_zero_matrix)
|
|
type = "type{}".format(int("{}{}".format(int(is_non_constant), int(is_non_homogeneous)), 2) + 1)
|
|
|
|
B = None
|
|
match.update({"type_of_equation": type, "antiderivative": B})
|
|
|
|
if is_non_constant:
|
|
B, is_commuting = _is_commutative_anti_derivative(A, t)
|
|
if not is_commuting:
|
|
raise NotImplementedError(filldedent('''
|
|
The system does not have a commutative antiderivative, it cannot be solved
|
|
by linodesolve.
|
|
'''))
|
|
|
|
match['antiderivative'] = B
|
|
match.update(_first_order_type5_6_subs(A, t, b=b))
|
|
|
|
return match
|
|
|
|
|
|
def _first_order_type5_6_subs(A, t, b=None):
|
|
match = {}
|
|
|
|
factor_terms = _factor_matrix(A, t)
|
|
is_homogeneous = b is None or b.is_zero_matrix
|
|
|
|
if factor_terms is not None:
|
|
t_ = Symbol("{}_".format(t))
|
|
F_t = integrate(factor_terms[0], t)
|
|
inverse = solveset(Eq(t_, F_t), t)
|
|
|
|
# Note: A simple way to check if a function is invertible
|
|
# or not.
|
|
if isinstance(inverse, FiniteSet) and not inverse.has(Piecewise)\
|
|
and len(inverse) == 1:
|
|
|
|
A = factor_terms[1]
|
|
if not is_homogeneous:
|
|
b = b / factor_terms[0]
|
|
b = b.subs(t, list(inverse)[0])
|
|
type = "type{}".format(5 + (not is_homogeneous))
|
|
match.update({'func_coeff': A, 'tau': F_t,
|
|
't_': t_, 'type_of_equation': type, 'rhs': b})
|
|
|
|
return match
|
|
|
|
|
|
def linear_ode_to_matrix(eqs, funcs, t, order):
|
|
r"""
|
|
Convert a linear system of ODEs to matrix form
|
|
|
|
Explanation
|
|
===========
|
|
|
|
Express a system of linear ordinary differential equations as a single
|
|
matrix differential equation [1]. For example the system $x' = x + y + 1$
|
|
and $y' = x - y$ can be represented as
|
|
|
|
.. math:: A_1 X' = A_0 X + b
|
|
|
|
where $A_1$ and $A_0$ are $2 \times 2$ matrices and $b$, $X$ and $X'$ are
|
|
$2 \times 1$ matrices with $X = [x, y]^T$.
|
|
|
|
Higher-order systems are represented with additional matrices e.g. a
|
|
second-order system would look like
|
|
|
|
.. math:: A_2 X'' = A_1 X' + A_0 X + b
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Function, Symbol, Matrix, Eq
|
|
>>> from sympy.solvers.ode.systems import linear_ode_to_matrix
|
|
>>> t = Symbol('t')
|
|
>>> x = Function('x')
|
|
>>> y = Function('y')
|
|
|
|
We can create a system of linear ODEs like
|
|
|
|
>>> eqs = [
|
|
... Eq(x(t).diff(t), x(t) + y(t) + 1),
|
|
... Eq(y(t).diff(t), x(t) - y(t)),
|
|
... ]
|
|
>>> funcs = [x(t), y(t)]
|
|
>>> order = 1 # 1st order system
|
|
|
|
Now ``linear_ode_to_matrix`` can represent this as a matrix
|
|
differential equation.
|
|
|
|
>>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, order)
|
|
>>> A1
|
|
Matrix([
|
|
[1, 0],
|
|
[0, 1]])
|
|
>>> A0
|
|
Matrix([
|
|
[1, 1],
|
|
[1, -1]])
|
|
>>> b
|
|
Matrix([
|
|
[1],
|
|
[0]])
|
|
|
|
The original equations can be recovered from these matrices:
|
|
|
|
>>> eqs_mat = Matrix([eq.lhs - eq.rhs for eq in eqs])
|
|
>>> X = Matrix(funcs)
|
|
>>> A1 * X.diff(t) - A0 * X - b == eqs_mat
|
|
True
|
|
|
|
If the system of equations has a maximum order greater than the
|
|
order of the system specified, a ODEOrderError exception is raised.
|
|
|
|
>>> eqs = [Eq(x(t).diff(t, 2), x(t).diff(t) + x(t)), Eq(y(t).diff(t), y(t) + x(t))]
|
|
>>> linear_ode_to_matrix(eqs, funcs, t, 1)
|
|
Traceback (most recent call last):
|
|
...
|
|
ODEOrderError: Cannot represent system in 1-order form
|
|
|
|
If the system of equations is nonlinear, then ODENonlinearError is
|
|
raised.
|
|
|
|
>>> eqs = [Eq(x(t).diff(t), x(t) + y(t)), Eq(y(t).diff(t), y(t)**2 + x(t))]
|
|
>>> linear_ode_to_matrix(eqs, funcs, t, 1)
|
|
Traceback (most recent call last):
|
|
...
|
|
ODENonlinearError: The system of ODEs is nonlinear.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
eqs : list of SymPy expressions or equalities
|
|
The equations as expressions (assumed equal to zero).
|
|
funcs : list of applied functions
|
|
The dependent variables of the system of ODEs.
|
|
t : symbol
|
|
The independent variable.
|
|
order : int
|
|
The order of the system of ODEs.
|
|
|
|
Returns
|
|
=======
|
|
|
|
The tuple ``(As, b)`` where ``As`` is a tuple of matrices and ``b`` is the
|
|
the matrix representing the rhs of the matrix equation.
|
|
|
|
Raises
|
|
======
|
|
|
|
ODEOrderError
|
|
When the system of ODEs have an order greater than what was specified
|
|
ODENonlinearError
|
|
When the system of ODEs is nonlinear
|
|
|
|
See Also
|
|
========
|
|
|
|
linear_eq_to_matrix: for systems of linear algebraic equations.
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://en.wikipedia.org/wiki/Matrix_differential_equation
|
|
|
|
"""
|
|
from sympy.solvers.solveset import linear_eq_to_matrix
|
|
|
|
if any(ode_order(eq, func) > order for eq in eqs for func in funcs):
|
|
msg = "Cannot represent system in {}-order form"
|
|
raise ODEOrderError(msg.format(order))
|
|
|
|
As = []
|
|
|
|
for o in range(order, -1, -1):
|
|
# Work from the highest derivative down
|
|
syms = [func.diff(t, o) for func in funcs]
|
|
|
|
# Ai is the matrix for X(t).diff(t, o)
|
|
# eqs is minus the remainder of the equations.
|
|
try:
|
|
Ai, b = linear_eq_to_matrix(eqs, syms)
|
|
except NonlinearError:
|
|
raise ODENonlinearError("The system of ODEs is nonlinear.")
|
|
|
|
Ai = Ai.applyfunc(expand_mul)
|
|
|
|
As.append(Ai if o == order else -Ai)
|
|
|
|
if o:
|
|
eqs = [-eq for eq in b]
|
|
else:
|
|
rhs = b
|
|
|
|
return As, rhs
|
|
|
|
|
|
def matrix_exp(A, t):
|
|
r"""
|
|
Matrix exponential $\exp(A*t)$ for the matrix ``A`` and scalar ``t``.
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This functions returns the $\exp(A*t)$ by doing a simple
|
|
matrix multiplication:
|
|
|
|
.. math:: \exp(A*t) = P * expJ * P^{-1}
|
|
|
|
where $expJ$ is $\exp(J*t)$. $J$ is the Jordan normal
|
|
form of $A$ and $P$ is matrix such that:
|
|
|
|
.. math:: A = P * J * P^{-1}
|
|
|
|
The matrix exponential $\exp(A*t)$ appears in the solution of linear
|
|
differential equations. For example if $x$ is a vector and $A$ is a matrix
|
|
then the initial value problem
|
|
|
|
.. math:: \frac{dx(t)}{dt} = A \times x(t), x(0) = x0
|
|
|
|
has the unique solution
|
|
|
|
.. math:: x(t) = \exp(A t) x0
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Symbol, Matrix, pprint
|
|
>>> from sympy.solvers.ode.systems import matrix_exp
|
|
>>> t = Symbol('t')
|
|
|
|
We will consider a 2x2 matrix for comupting the exponential
|
|
|
|
>>> A = Matrix([[2, -5], [2, -4]])
|
|
>>> pprint(A)
|
|
[2 -5]
|
|
[ ]
|
|
[2 -4]
|
|
|
|
Now, exp(A*t) is given as follows:
|
|
|
|
>>> pprint(matrix_exp(A, t))
|
|
[ -t -t -t ]
|
|
[3*e *sin(t) + e *cos(t) -5*e *sin(t) ]
|
|
[ ]
|
|
[ -t -t -t ]
|
|
[ 2*e *sin(t) - 3*e *sin(t) + e *cos(t)]
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A : Matrix
|
|
The matrix $A$ in the expression $\exp(A*t)$
|
|
t : Symbol
|
|
The independent variable
|
|
|
|
See Also
|
|
========
|
|
|
|
matrix_exp_jordan_form: For exponential of Jordan normal form
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://en.wikipedia.org/wiki/Jordan_normal_form
|
|
.. [2] https://en.wikipedia.org/wiki/Matrix_exponential
|
|
|
|
"""
|
|
P, expJ = matrix_exp_jordan_form(A, t)
|
|
return P * expJ * P.inv()
|
|
|
|
|
|
def matrix_exp_jordan_form(A, t):
|
|
r"""
|
|
Matrix exponential $\exp(A*t)$ for the matrix *A* and scalar *t*.
|
|
|
|
Explanation
|
|
===========
|
|
|
|
Returns the Jordan form of the $\exp(A*t)$ along with the matrix $P$ such that:
|
|
|
|
.. math::
|
|
\exp(A*t) = P * expJ * P^{-1}
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Matrix, Symbol
|
|
>>> from sympy.solvers.ode.systems import matrix_exp, matrix_exp_jordan_form
|
|
>>> t = Symbol('t')
|
|
|
|
We will consider a 2x2 defective matrix. This shows that our method
|
|
works even for defective matrices.
|
|
|
|
>>> A = Matrix([[1, 1], [0, 1]])
|
|
|
|
It can be observed that this function gives us the Jordan normal form
|
|
and the required invertible matrix P.
|
|
|
|
>>> P, expJ = matrix_exp_jordan_form(A, t)
|
|
|
|
Here, it is shown that P and expJ returned by this function is correct
|
|
as they satisfy the formula: P * expJ * P_inverse = exp(A*t).
|
|
|
|
>>> P * expJ * P.inv() == matrix_exp(A, t)
|
|
True
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A : Matrix
|
|
The matrix $A$ in the expression $\exp(A*t)$
|
|
t : Symbol
|
|
The independent variable
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://en.wikipedia.org/wiki/Defective_matrix
|
|
.. [2] https://en.wikipedia.org/wiki/Jordan_matrix
|
|
.. [3] https://en.wikipedia.org/wiki/Jordan_normal_form
|
|
|
|
"""
|
|
|
|
N, M = A.shape
|
|
if N != M:
|
|
raise ValueError('Needed square matrix but got shape (%s, %s)' % (N, M))
|
|
elif A.has(t):
|
|
raise ValueError('Matrix A should not depend on t')
|
|
|
|
def jordan_chains(A):
|
|
'''Chains from Jordan normal form analogous to M.eigenvects().
|
|
Returns a dict with eignevalues as keys like:
|
|
{e1: [[v111,v112,...], [v121, v122,...]], e2:...}
|
|
where vijk is the kth vector in the jth chain for eigenvalue i.
|
|
'''
|
|
P, blocks = A.jordan_cells()
|
|
basis = [P[:,i] for i in range(P.shape[1])]
|
|
n = 0
|
|
chains = {}
|
|
for b in blocks:
|
|
eigval = b[0, 0]
|
|
size = b.shape[0]
|
|
if eigval not in chains:
|
|
chains[eigval] = []
|
|
chains[eigval].append(basis[n:n+size])
|
|
n += size
|
|
return chains
|
|
|
|
eigenchains = jordan_chains(A)
|
|
|
|
# Needed for consistency across Python versions
|
|
eigenchains_iter = sorted(eigenchains.items(), key=default_sort_key)
|
|
isreal = not A.has(I)
|
|
|
|
blocks = []
|
|
vectors = []
|
|
seen_conjugate = set()
|
|
for e, chains in eigenchains_iter:
|
|
for chain in chains:
|
|
n = len(chain)
|
|
if isreal and e != e.conjugate() and e.conjugate() in eigenchains:
|
|
if e in seen_conjugate:
|
|
continue
|
|
seen_conjugate.add(e.conjugate())
|
|
exprt = exp(re(e) * t)
|
|
imrt = im(e) * t
|
|
imblock = Matrix([[cos(imrt), sin(imrt)],
|
|
[-sin(imrt), cos(imrt)]])
|
|
expJblock2 = Matrix(n, n, lambda i,j:
|
|
imblock * t**(j-i) / factorial(j-i) if j >= i
|
|
else zeros(2, 2))
|
|
expJblock = Matrix(2*n, 2*n, lambda i,j: expJblock2[i//2,j//2][i%2,j%2])
|
|
|
|
blocks.append(exprt * expJblock)
|
|
for i in range(n):
|
|
vectors.append(re(chain[i]))
|
|
vectors.append(im(chain[i]))
|
|
else:
|
|
vectors.extend(chain)
|
|
fun = lambda i,j: t**(j-i)/factorial(j-i) if j >= i else 0
|
|
expJblock = Matrix(n, n, fun)
|
|
blocks.append(exp(e * t) * expJblock)
|
|
|
|
expJ = Matrix.diag(*blocks)
|
|
P = Matrix(N, N, lambda i,j: vectors[j][i])
|
|
|
|
return P, expJ
|
|
|
|
|
|
# Note: To add a docstring example with tau
|
|
def linodesolve(A, t, b=None, B=None, type="auto", doit=False,
|
|
tau=None):
|
|
r"""
|
|
System of n equations linear first-order differential equations
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This solver solves the system of ODEs of the following form:
|
|
|
|
.. math::
|
|
X'(t) = A(t) X(t) + b(t)
|
|
|
|
Here, $A(t)$ is the coefficient matrix, $X(t)$ is the vector of n independent variables,
|
|
$b(t)$ is the non-homogeneous term and $X'(t)$ is the derivative of $X(t)$
|
|
|
|
Depending on the properties of $A(t)$ and $b(t)$, this solver evaluates the solution
|
|
differently.
|
|
|
|
When $A(t)$ is constant coefficient matrix and $b(t)$ is zero vector i.e. system is homogeneous,
|
|
the system is "type1". The solution is:
|
|
|
|
.. math::
|
|
X(t) = \exp(A t) C
|
|
|
|
Here, $C$ is a vector of constants and $A$ is the constant coefficient matrix.
|
|
|
|
When $A(t)$ is constant coefficient matrix and $b(t)$ is non-zero i.e. system is non-homogeneous,
|
|
the system is "type2". The solution is:
|
|
|
|
.. math::
|
|
X(t) = e^{A t} ( \int e^{- A t} b \,dt + C)
|
|
|
|
When $A(t)$ is coefficient matrix such that its commutative with its antiderivative $B(t)$ and
|
|
$b(t)$ is a zero vector i.e. system is homogeneous, the system is "type3". The solution is:
|
|
|
|
.. math::
|
|
X(t) = \exp(B(t)) C
|
|
|
|
When $A(t)$ is commutative with its antiderivative $B(t)$ and $b(t)$ is non-zero i.e. system is
|
|
non-homogeneous, the system is "type4". The solution is:
|
|
|
|
.. math::
|
|
X(t) = e^{B(t)} ( \int e^{-B(t)} b(t) \,dt + C)
|
|
|
|
When $A(t)$ is a coefficient matrix such that it can be factorized into a scalar and a constant
|
|
coefficient matrix:
|
|
|
|
.. math::
|
|
A(t) = f(t) * A
|
|
|
|
Where $f(t)$ is a scalar expression in the independent variable $t$ and $A$ is a constant matrix,
|
|
then we can do the following substitutions:
|
|
|
|
.. math::
|
|
tau = \int f(t) dt, X(t) = Y(tau), b(t) = b(f^{-1}(tau))
|
|
|
|
Here, the substitution for the non-homogeneous term is done only when its non-zero.
|
|
Using these substitutions, our original system becomes:
|
|
|
|
.. math::
|
|
Y'(tau) = A * Y(tau) + b(tau)/f(tau)
|
|
|
|
The above system can be easily solved using the solution for "type1" or "type2" depending
|
|
on the homogeneity of the system. After we get the solution for $Y(tau)$, we substitute the
|
|
solution for $tau$ as $t$ to get back $X(t)$
|
|
|
|
.. math::
|
|
X(t) = Y(tau)
|
|
|
|
Systems of "type5" and "type6" have a commutative antiderivative but we use this solution
|
|
because its faster to compute.
|
|
|
|
The final solution is the general solution for all the four equations since a constant coefficient
|
|
matrix is always commutative with its antidervative.
|
|
|
|
An additional feature of this function is, if someone wants to substitute for value of the independent
|
|
variable, they can pass the substitution `tau` and the solution will have the independent variable
|
|
substituted with the passed expression(`tau`).
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A : Matrix
|
|
Coefficient matrix of the system of linear first order ODEs.
|
|
t : Symbol
|
|
Independent variable in the system of ODEs.
|
|
b : Matrix or None
|
|
Non-homogeneous term in the system of ODEs. If None is passed,
|
|
a homogeneous system of ODEs is assumed.
|
|
B : Matrix or None
|
|
Antiderivative of the coefficient matrix. If the antiderivative
|
|
is not passed and the solution requires the term, then the solver
|
|
would compute it internally.
|
|
type : String
|
|
Type of the system of ODEs passed. Depending on the type, the
|
|
solution is evaluated. The type values allowed and the corresponding
|
|
system it solves are: "type1" for constant coefficient homogeneous
|
|
"type2" for constant coefficient non-homogeneous, "type3" for non-constant
|
|
coefficient homogeneous, "type4" for non-constant coefficient non-homogeneous,
|
|
"type5" and "type6" for non-constant coefficient homogeneous and non-homogeneous
|
|
systems respectively where the coefficient matrix can be factorized to a constant
|
|
coefficient matrix.
|
|
The default value is "auto" which will let the solver decide the correct type of
|
|
the system passed.
|
|
doit : Boolean
|
|
Evaluate the solution if True, default value is False
|
|
tau: Expression
|
|
Used to substitute for the value of `t` after we get the solution of the system.
|
|
|
|
Examples
|
|
========
|
|
|
|
To solve the system of ODEs using this function directly, several things must be
|
|
done in the right order. Wrong inputs to the function will lead to incorrect results.
|
|
|
|
>>> from sympy import symbols, Function, Eq
|
|
>>> from sympy.solvers.ode.systems import canonical_odes, linear_ode_to_matrix, linodesolve, linodesolve_type
|
|
>>> from sympy.solvers.ode.subscheck import checkodesol
|
|
>>> f, g = symbols("f, g", cls=Function)
|
|
>>> x, a = symbols("x, a")
|
|
>>> funcs = [f(x), g(x)]
|
|
>>> eqs = [Eq(f(x).diff(x) - f(x), a*g(x) + 1), Eq(g(x).diff(x) + g(x), a*f(x))]
|
|
|
|
Here, it is important to note that before we derive the coefficient matrix, it is
|
|
important to get the system of ODEs into the desired form. For that we will use
|
|
:obj:`sympy.solvers.ode.systems.canonical_odes()`.
|
|
|
|
>>> eqs = canonical_odes(eqs, funcs, x)
|
|
>>> eqs
|
|
[[Eq(Derivative(f(x), x), a*g(x) + f(x) + 1), Eq(Derivative(g(x), x), a*f(x) - g(x))]]
|
|
|
|
Now, we will use :obj:`sympy.solvers.ode.systems.linear_ode_to_matrix()` to get the coefficient matrix and the
|
|
non-homogeneous term if it is there.
|
|
|
|
>>> eqs = eqs[0]
|
|
>>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, x, 1)
|
|
>>> A = A0
|
|
|
|
We have the coefficient matrices and the non-homogeneous term ready. Now, we can use
|
|
:obj:`sympy.solvers.ode.systems.linodesolve_type()` to get the information for the system of ODEs
|
|
to finally pass it to the solver.
|
|
|
|
>>> system_info = linodesolve_type(A, x, b=b)
|
|
>>> sol_vector = linodesolve(A, x, b=b, B=system_info['antiderivative'], type=system_info['type_of_equation'])
|
|
|
|
Now, we can prove if the solution is correct or not by using :obj:`sympy.solvers.ode.checkodesol()`
|
|
|
|
>>> sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)]
|
|
>>> checkodesol(eqs, sol)
|
|
(True, [0, 0])
|
|
|
|
We can also use the doit method to evaluate the solutions passed by the function.
|
|
|
|
>>> sol_vector_evaluated = linodesolve(A, x, b=b, type="type2", doit=True)
|
|
|
|
Now, we will look at a system of ODEs which is non-constant.
|
|
|
|
>>> eqs = [Eq(f(x).diff(x), f(x) + x*g(x)), Eq(g(x).diff(x), -x*f(x) + g(x))]
|
|
|
|
The system defined above is already in the desired form, so we do not have to convert it.
|
|
|
|
>>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, x, 1)
|
|
>>> A = A0
|
|
|
|
A user can also pass the commutative antiderivative required for type3 and type4 system of ODEs.
|
|
Passing an incorrect one will lead to incorrect results. If the coefficient matrix is not commutative
|
|
with its antiderivative, then :obj:`sympy.solvers.ode.systems.linodesolve_type()` raises a NotImplementedError.
|
|
If it does have a commutative antiderivative, then the function just returns the information about the system.
|
|
|
|
>>> system_info = linodesolve_type(A, x, b=b)
|
|
|
|
Now, we can pass the antiderivative as an argument to get the solution. If the system information is not
|
|
passed, then the solver will compute the required arguments internally.
|
|
|
|
>>> sol_vector = linodesolve(A, x, b=b)
|
|
|
|
Once again, we can verify the solution obtained.
|
|
|
|
>>> sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)]
|
|
>>> checkodesol(eqs, sol)
|
|
(True, [0, 0])
|
|
|
|
Returns
|
|
=======
|
|
|
|
List
|
|
|
|
Raises
|
|
======
|
|
|
|
ValueError
|
|
This error is raised when the coefficient matrix, non-homogeneous term
|
|
or the antiderivative, if passed, are not a matrix or
|
|
do not have correct dimensions
|
|
NonSquareMatrixError
|
|
When the coefficient matrix or its antiderivative, if passed is not a
|
|
square matrix
|
|
NotImplementedError
|
|
If the coefficient matrix does not have a commutative antiderivative
|
|
|
|
See Also
|
|
========
|
|
|
|
linear_ode_to_matrix: Coefficient matrix computation function
|
|
canonical_odes: System of ODEs representation change
|
|
linodesolve_type: Getting information about systems of ODEs to pass in this solver
|
|
|
|
"""
|
|
|
|
if not isinstance(A, MatrixBase):
|
|
raise ValueError(filldedent('''\
|
|
The coefficients of the system of ODEs should be of type Matrix
|
|
'''))
|
|
|
|
if not A.is_square:
|
|
raise NonSquareMatrixError(filldedent('''\
|
|
The coefficient matrix must be a square
|
|
'''))
|
|
|
|
if b is not None:
|
|
if not isinstance(b, MatrixBase):
|
|
raise ValueError(filldedent('''\
|
|
The non-homogeneous terms of the system of ODEs should be of type Matrix
|
|
'''))
|
|
|
|
if A.rows != b.rows:
|
|
raise ValueError(filldedent('''\
|
|
The system of ODEs should have the same number of non-homogeneous terms and the number of
|
|
equations
|
|
'''))
|
|
|
|
if B is not None:
|
|
if not isinstance(B, MatrixBase):
|
|
raise ValueError(filldedent('''\
|
|
The antiderivative of coefficients of the system of ODEs should be of type Matrix
|
|
'''))
|
|
|
|
if not B.is_square:
|
|
raise NonSquareMatrixError(filldedent('''\
|
|
The antiderivative of the coefficient matrix must be a square
|
|
'''))
|
|
|
|
if A.rows != B.rows:
|
|
raise ValueError(filldedent('''\
|
|
The coefficient matrix and its antiderivative should have same dimensions
|
|
'''))
|
|
|
|
if not any(type == "type{}".format(i) for i in range(1, 7)) and not type == "auto":
|
|
raise ValueError(filldedent('''\
|
|
The input type should be a valid one
|
|
'''))
|
|
|
|
n = A.rows
|
|
|
|
# constants = numbered_symbols(prefix='C', cls=Dummy, start=const_idx+1)
|
|
Cvect = Matrix([Dummy() for _ in range(n)])
|
|
|
|
if b is None and any(type == typ for typ in ["type2", "type4", "type6"]):
|
|
b = zeros(n, 1)
|
|
|
|
is_transformed = tau is not None
|
|
passed_type = type
|
|
|
|
if type == "auto":
|
|
system_info = linodesolve_type(A, t, b=b)
|
|
type = system_info["type_of_equation"]
|
|
B = system_info["antiderivative"]
|
|
|
|
if type in ("type5", "type6"):
|
|
is_transformed = True
|
|
if passed_type != "auto":
|
|
if tau is None:
|
|
system_info = _first_order_type5_6_subs(A, t, b=b)
|
|
if not system_info:
|
|
raise ValueError(filldedent('''
|
|
The system passed isn't {}.
|
|
'''.format(type)))
|
|
|
|
tau = system_info['tau']
|
|
t = system_info['t_']
|
|
A = system_info['A']
|
|
b = system_info['b']
|
|
|
|
intx_wrtt = lambda x: Integral(x, t) if x else 0
|
|
if type in ("type1", "type2", "type5", "type6"):
|
|
P, J = matrix_exp_jordan_form(A, t)
|
|
P = simplify(P)
|
|
|
|
if type in ("type1", "type5"):
|
|
sol_vector = P * (J * Cvect)
|
|
else:
|
|
Jinv = J.subs(t, -t)
|
|
sol_vector = P * J * ((Jinv * P.inv() * b).applyfunc(intx_wrtt) + Cvect)
|
|
else:
|
|
if B is None:
|
|
B, _ = _is_commutative_anti_derivative(A, t)
|
|
|
|
if type == "type3":
|
|
sol_vector = B.exp() * Cvect
|
|
else:
|
|
sol_vector = B.exp() * (((-B).exp() * b).applyfunc(intx_wrtt) + Cvect)
|
|
|
|
if is_transformed:
|
|
sol_vector = sol_vector.subs(t, tau)
|
|
|
|
gens = sol_vector.atoms(exp)
|
|
|
|
if type != "type1":
|
|
sol_vector = [expand_mul(s) for s in sol_vector]
|
|
|
|
sol_vector = [collect(s, ordered(gens), exact=True) for s in sol_vector]
|
|
|
|
if doit:
|
|
sol_vector = [s.doit() for s in sol_vector]
|
|
|
|
return sol_vector
|
|
|
|
|
|
def _matrix_is_constant(M, t):
|
|
"""Checks if the matrix M is independent of t or not."""
|
|
return all(coef.as_independent(t, as_Add=True)[1] == 0 for coef in M)
|
|
|
|
|
|
def canonical_odes(eqs, funcs, t):
|
|
r"""
|
|
Function that solves for highest order derivatives in a system
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function inputs a system of ODEs and based on the system,
|
|
the dependent variables and their highest order, returns the system
|
|
in the following form:
|
|
|
|
.. math::
|
|
X'(t) = A(t) X(t) + b(t)
|
|
|
|
Here, $X(t)$ is the vector of dependent variables of lower order, $A(t)$ is
|
|
the coefficient matrix, $b(t)$ is the non-homogeneous term and $X'(t)$ is the
|
|
vector of dependent variables in their respective highest order. We use the term
|
|
canonical form to imply the system of ODEs which is of the above form.
|
|
|
|
If the system passed has a non-linear term with multiple solutions, then a list of
|
|
systems is returned in its canonical form.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
eqs : List
|
|
List of the ODEs
|
|
funcs : List
|
|
List of dependent variables
|
|
t : Symbol
|
|
Independent variable
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import symbols, Function, Eq, Derivative
|
|
>>> from sympy.solvers.ode.systems import canonical_odes
|
|
>>> f, g = symbols("f g", cls=Function)
|
|
>>> x, y = symbols("x y")
|
|
>>> funcs = [f(x), g(x)]
|
|
>>> eqs = [Eq(f(x).diff(x) - 7*f(x), 12*g(x)), Eq(g(x).diff(x) + g(x), 20*f(x))]
|
|
|
|
>>> canonical_eqs = canonical_odes(eqs, funcs, x)
|
|
>>> canonical_eqs
|
|
[[Eq(Derivative(f(x), x), 7*f(x) + 12*g(x)), Eq(Derivative(g(x), x), 20*f(x) - g(x))]]
|
|
|
|
>>> system = [Eq(Derivative(f(x), x)**2 - 2*Derivative(f(x), x) + 1, 4), Eq(-y*f(x) + Derivative(g(x), x), 0)]
|
|
|
|
>>> canonical_system = canonical_odes(system, funcs, x)
|
|
>>> canonical_system
|
|
[[Eq(Derivative(f(x), x), -1), Eq(Derivative(g(x), x), y*f(x))], [Eq(Derivative(f(x), x), 3), Eq(Derivative(g(x), x), y*f(x))]]
|
|
|
|
Returns
|
|
=======
|
|
|
|
List
|
|
|
|
"""
|
|
from sympy.solvers.solvers import solve
|
|
|
|
order = _get_func_order(eqs, funcs)
|
|
|
|
canon_eqs = solve(eqs, *[func.diff(t, order[func]) for func in funcs], dict=True)
|
|
|
|
systems = []
|
|
for eq in canon_eqs:
|
|
system = [Eq(func.diff(t, order[func]), eq[func.diff(t, order[func])]) for func in funcs]
|
|
systems.append(system)
|
|
|
|
return systems
|
|
|
|
|
|
def _is_commutative_anti_derivative(A, t):
|
|
r"""
|
|
Helper function for determining if the Matrix passed is commutative with its antiderivative
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function checks if the Matrix $A$ passed is commutative with its antiderivative with respect
|
|
to the independent variable $t$.
|
|
|
|
.. math::
|
|
B(t) = \int A(t) dt
|
|
|
|
The function outputs two values, first one being the antiderivative $B(t)$, second one being a
|
|
boolean value, if True, then the matrix $A(t)$ passed is commutative with $B(t)$, else the matrix
|
|
passed isn't commutative with $B(t)$.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A : Matrix
|
|
The matrix which has to be checked
|
|
t : Symbol
|
|
Independent variable
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import symbols, Matrix
|
|
>>> from sympy.solvers.ode.systems import _is_commutative_anti_derivative
|
|
>>> t = symbols("t")
|
|
>>> A = Matrix([[1, t], [-t, 1]])
|
|
|
|
>>> B, is_commuting = _is_commutative_anti_derivative(A, t)
|
|
>>> is_commuting
|
|
True
|
|
|
|
Returns
|
|
=======
|
|
|
|
Matrix, Boolean
|
|
|
|
"""
|
|
B = integrate(A, t)
|
|
is_commuting = (B*A - A*B).applyfunc(expand).applyfunc(factor_terms).is_zero_matrix
|
|
|
|
is_commuting = False if is_commuting is None else is_commuting
|
|
|
|
return B, is_commuting
|
|
|
|
|
|
def _factor_matrix(A, t):
|
|
term = None
|
|
for element in A:
|
|
temp_term = element.as_independent(t)[1]
|
|
if temp_term.has(t):
|
|
term = temp_term
|
|
break
|
|
|
|
if term is not None:
|
|
A_factored = (A/term).applyfunc(ratsimp)
|
|
can_factor = _matrix_is_constant(A_factored, t)
|
|
term = (term, A_factored) if can_factor else None
|
|
|
|
return term
|
|
|
|
|
|
def _is_second_order_type2(A, t):
|
|
term = _factor_matrix(A, t)
|
|
is_type2 = False
|
|
|
|
if term is not None:
|
|
term = 1/term[0]
|
|
is_type2 = term.is_polynomial()
|
|
|
|
if is_type2:
|
|
poly = Poly(term.expand(), t)
|
|
monoms = poly.monoms()
|
|
|
|
if monoms[0][0] in (2, 4):
|
|
cs = _get_poly_coeffs(poly, 4)
|
|
a, b, c, d, e = cs
|
|
|
|
a1 = powdenest(sqrt(a), force=True)
|
|
c1 = powdenest(sqrt(e), force=True)
|
|
b1 = powdenest(sqrt(c - 2*a1*c1), force=True)
|
|
|
|
is_type2 = (b == 2*a1*b1) and (d == 2*b1*c1)
|
|
term = a1*t**2 + b1*t + c1
|
|
|
|
else:
|
|
is_type2 = False
|
|
|
|
return is_type2, term
|
|
|
|
|
|
def _get_poly_coeffs(poly, order):
|
|
cs = [0 for _ in range(order+1)]
|
|
for c, m in zip(poly.coeffs(), poly.monoms()):
|
|
cs[-1-m[0]] = c
|
|
return cs
|
|
|
|
|
|
def _match_second_order_type(A1, A0, t, b=None):
|
|
r"""
|
|
Works only for second order system in its canonical form.
|
|
|
|
Type 0: Constant coefficient matrix, can be simply solved by
|
|
introducing dummy variables.
|
|
Type 1: When the substitution: $U = t*X' - X$ works for reducing
|
|
the second order system to first order system.
|
|
Type 2: When the system is of the form: $poly * X'' = A*X$ where
|
|
$poly$ is square of a quadratic polynomial with respect to
|
|
*t* and $A$ is a constant coefficient matrix.
|
|
|
|
"""
|
|
match = {"type_of_equation": "type0"}
|
|
n = A1.shape[0]
|
|
|
|
if _matrix_is_constant(A1, t) and _matrix_is_constant(A0, t):
|
|
return match
|
|
|
|
if (A1 + A0*t).applyfunc(expand_mul).is_zero_matrix:
|
|
match.update({"type_of_equation": "type1", "A1": A1})
|
|
|
|
elif A1.is_zero_matrix and (b is None or b.is_zero_matrix):
|
|
is_type2, term = _is_second_order_type2(A0, t)
|
|
if is_type2:
|
|
a, b, c = _get_poly_coeffs(Poly(term, t), 2)
|
|
A = (A0*(term**2).expand()).applyfunc(ratsimp) + (b**2/4 - a*c)*eye(n, n)
|
|
tau = integrate(1/term, t)
|
|
t_ = Symbol("{}_".format(t))
|
|
match.update({"type_of_equation": "type2", "A0": A,
|
|
"g(t)": sqrt(term), "tau": tau, "is_transformed": True,
|
|
"t_": t_})
|
|
|
|
return match
|
|
|
|
|
|
def _second_order_subs_type1(A, b, funcs, t):
|
|
r"""
|
|
For a linear, second order system of ODEs, a particular substitution.
|
|
|
|
A system of the below form can be reduced to a linear first order system of
|
|
ODEs:
|
|
.. math::
|
|
X'' = A(t) * (t*X' - X) + b(t)
|
|
|
|
By substituting:
|
|
.. math:: U = t*X' - X
|
|
|
|
To get the system:
|
|
.. math:: U' = t*(A(t)*U + b(t))
|
|
|
|
Where $U$ is the vector of dependent variables, $X$ is the vector of dependent
|
|
variables in `funcs` and $X'$ is the first order derivative of $X$ with respect to
|
|
$t$. It may or may not reduce the system into linear first order system of ODEs.
|
|
|
|
Then a check is made to determine if the system passed can be reduced or not, if
|
|
this substitution works, then the system is reduced and its solved for the new
|
|
substitution. After we get the solution for $U$:
|
|
|
|
.. math:: U = a(t)
|
|
|
|
We substitute and return the reduced system:
|
|
|
|
.. math::
|
|
a(t) = t*X' - X
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A: Matrix
|
|
Coefficient matrix($A(t)*t$) of the second order system of this form.
|
|
b: Matrix
|
|
Non-homogeneous term($b(t)$) of the system of ODEs.
|
|
funcs: List
|
|
List of dependent variables
|
|
t: Symbol
|
|
Independent variable of the system of ODEs.
|
|
|
|
Returns
|
|
=======
|
|
|
|
List
|
|
|
|
"""
|
|
|
|
U = Matrix([t*func.diff(t) - func for func in funcs])
|
|
|
|
sol = linodesolve(A, t, t*b)
|
|
reduced_eqs = [Eq(u, s) for s, u in zip(sol, U)]
|
|
reduced_eqs = canonical_odes(reduced_eqs, funcs, t)[0]
|
|
|
|
return reduced_eqs
|
|
|
|
|
|
def _second_order_subs_type2(A, funcs, t_):
|
|
r"""
|
|
Returns a second order system based on the coefficient matrix passed.
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function returns a system of second order ODE of the following form:
|
|
|
|
.. math::
|
|
X'' = A * X
|
|
|
|
Here, $X$ is the vector of dependent variables, but a bit modified, $A$ is the
|
|
coefficient matrix passed.
|
|
|
|
Along with returning the second order system, this function also returns the new
|
|
dependent variables with the new independent variable `t_` passed.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A: Matrix
|
|
Coefficient matrix of the system
|
|
funcs: List
|
|
List of old dependent variables
|
|
t_: Symbol
|
|
New independent variable
|
|
|
|
Returns
|
|
=======
|
|
|
|
List, List
|
|
|
|
"""
|
|
func_names = [func.func.__name__ for func in funcs]
|
|
new_funcs = [Function(Dummy("{}_".format(name)))(t_) for name in func_names]
|
|
rhss = A * Matrix(new_funcs)
|
|
new_eqs = [Eq(func.diff(t_, 2), rhs) for func, rhs in zip(new_funcs, rhss)]
|
|
|
|
return new_eqs, new_funcs
|
|
|
|
|
|
def _is_euler_system(As, t):
|
|
return all(_matrix_is_constant((A*t**i).applyfunc(ratsimp), t) for i, A in enumerate(As))
|
|
|
|
|
|
def _classify_linear_system(eqs, funcs, t, is_canon=False):
|
|
r"""
|
|
Returns a dictionary with details of the eqs if the system passed is linear
|
|
and can be classified by this function else returns None
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function takes the eqs, converts it into a form Ax = b where x is a vector of terms
|
|
containing dependent variables and their derivatives till their maximum order. If it is
|
|
possible to convert eqs into Ax = b, then all the equations in eqs are linear otherwise
|
|
they are non-linear.
|
|
|
|
To check if the equations are constant coefficient, we need to check if all the terms in
|
|
A obtained above are constant or not.
|
|
|
|
To check if the equations are homogeneous or not, we need to check if b is a zero matrix
|
|
or not.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
eqs: List
|
|
List of ODEs
|
|
funcs: List
|
|
List of dependent variables
|
|
t: Symbol
|
|
Independent variable of the equations in eqs
|
|
is_canon: Boolean
|
|
If True, then this function will not try to get the
|
|
system in canonical form. Default value is False
|
|
|
|
Returns
|
|
=======
|
|
|
|
match = {
|
|
'no_of_equation': len(eqs),
|
|
'eq': eqs,
|
|
'func': funcs,
|
|
'order': order,
|
|
'is_linear': is_linear,
|
|
'is_constant': is_constant,
|
|
'is_homogeneous': is_homogeneous,
|
|
}
|
|
|
|
Dict or list of Dicts or None
|
|
Dict with values for keys:
|
|
1. no_of_equation: Number of equations
|
|
2. eq: The set of equations
|
|
3. func: List of dependent variables
|
|
4. order: A dictionary that gives the order of the
|
|
dependent variable in eqs
|
|
5. is_linear: Boolean value indicating if the set of
|
|
equations are linear or not.
|
|
6. is_constant: Boolean value indicating if the set of
|
|
equations have constant coefficients or not.
|
|
7. is_homogeneous: Boolean value indicating if the set of
|
|
equations are homogeneous or not.
|
|
8. commutative_antiderivative: Antiderivative of the coefficient
|
|
matrix if the coefficient matrix is non-constant
|
|
and commutative with its antiderivative. This key
|
|
may or may not exist.
|
|
9. is_general: Boolean value indicating if the system of ODEs is
|
|
solvable using one of the general case solvers or not.
|
|
10. rhs: rhs of the non-homogeneous system of ODEs in Matrix form. This
|
|
key may or may not exist.
|
|
11. is_higher_order: True if the system passed has an order greater than 1.
|
|
This key may or may not exist.
|
|
12. is_second_order: True if the system passed is a second order ODE. This
|
|
key may or may not exist.
|
|
This Dict is the answer returned if the eqs are linear and constant
|
|
coefficient. Otherwise, None is returned.
|
|
|
|
"""
|
|
|
|
# Error for i == 0 can be added but isn't for now
|
|
|
|
# Check for len(funcs) == len(eqs)
|
|
if len(funcs) != len(eqs):
|
|
raise ValueError("Number of functions given is not equal to the number of equations %s" % funcs)
|
|
|
|
# ValueError when functions have more than one arguments
|
|
for func in funcs:
|
|
if len(func.args) != 1:
|
|
raise ValueError("dsolve() and classify_sysode() work with "
|
|
"functions of one variable only, not %s" % func)
|
|
|
|
# Getting the func_dict and order using the helper
|
|
# function
|
|
order = _get_func_order(eqs, funcs)
|
|
system_order = max(order[func] for func in funcs)
|
|
is_higher_order = system_order > 1
|
|
is_second_order = system_order == 2 and all(order[func] == 2 for func in funcs)
|
|
|
|
# Not adding the check if the len(func.args) for
|
|
# every func in funcs is 1
|
|
|
|
# Linearity check
|
|
try:
|
|
|
|
canon_eqs = canonical_odes(eqs, funcs, t) if not is_canon else [eqs]
|
|
if len(canon_eqs) == 1:
|
|
As, b = linear_ode_to_matrix(canon_eqs[0], funcs, t, system_order)
|
|
else:
|
|
|
|
match = {
|
|
'is_implicit': True,
|
|
'canon_eqs': canon_eqs
|
|
}
|
|
|
|
return match
|
|
|
|
# When the system of ODEs is non-linear, an ODENonlinearError is raised.
|
|
# This function catches the error and None is returned.
|
|
except ODENonlinearError:
|
|
return None
|
|
|
|
is_linear = True
|
|
|
|
# Homogeneous check
|
|
is_homogeneous = True if b.is_zero_matrix else False
|
|
|
|
# Is general key is used to identify if the system of ODEs can be solved by
|
|
# one of the general case solvers or not.
|
|
match = {
|
|
'no_of_equation': len(eqs),
|
|
'eq': eqs,
|
|
'func': funcs,
|
|
'order': order,
|
|
'is_linear': is_linear,
|
|
'is_homogeneous': is_homogeneous,
|
|
'is_general': True
|
|
}
|
|
|
|
if not is_homogeneous:
|
|
match['rhs'] = b
|
|
|
|
is_constant = all(_matrix_is_constant(A_, t) for A_ in As)
|
|
|
|
# The match['is_linear'] check will be added in the future when this
|
|
# function becomes ready to deal with non-linear systems of ODEs
|
|
|
|
if not is_higher_order:
|
|
A = As[1]
|
|
match['func_coeff'] = A
|
|
|
|
# Constant coefficient check
|
|
is_constant = _matrix_is_constant(A, t)
|
|
match['is_constant'] = is_constant
|
|
|
|
try:
|
|
system_info = linodesolve_type(A, t, b=b)
|
|
except NotImplementedError:
|
|
return None
|
|
|
|
match.update(system_info)
|
|
antiderivative = match.pop("antiderivative")
|
|
|
|
if not is_constant:
|
|
match['commutative_antiderivative'] = antiderivative
|
|
|
|
return match
|
|
else:
|
|
match['type_of_equation'] = "type0"
|
|
|
|
if is_second_order:
|
|
A1, A0 = As[1:]
|
|
|
|
match_second_order = _match_second_order_type(A1, A0, t)
|
|
match.update(match_second_order)
|
|
|
|
match['is_second_order'] = True
|
|
|
|
# If system is constant, then no need to check if its in euler
|
|
# form or not. It will be easier and faster to directly proceed
|
|
# to solve it.
|
|
if match['type_of_equation'] == "type0" and not is_constant:
|
|
is_euler = _is_euler_system(As, t)
|
|
if is_euler:
|
|
t_ = Symbol('{}_'.format(t))
|
|
match.update({'is_transformed': True, 'type_of_equation': 'type1',
|
|
't_': t_})
|
|
else:
|
|
is_jordan = lambda M: M == Matrix.jordan_block(M.shape[0], M[0, 0])
|
|
terms = _factor_matrix(As[-1], t)
|
|
if all(A.is_zero_matrix for A in As[1:-1]) and terms is not None and not is_jordan(terms[1]):
|
|
P, J = terms[1].jordan_form()
|
|
match.update({'type_of_equation': 'type2', 'J': J,
|
|
'f(t)': terms[0], 'P': P, 'is_transformed': True})
|
|
|
|
if match['type_of_equation'] != 'type0' and is_second_order:
|
|
match.pop('is_second_order', None)
|
|
|
|
match['is_higher_order'] = is_higher_order
|
|
|
|
return match
|
|
|
|
def _preprocess_eqs(eqs):
|
|
processed_eqs = []
|
|
for eq in eqs:
|
|
processed_eqs.append(eq if isinstance(eq, Equality) else Eq(eq, 0))
|
|
|
|
return processed_eqs
|
|
|
|
|
|
def _eqs2dict(eqs, funcs):
|
|
eqsorig = {}
|
|
eqsmap = {}
|
|
funcset = set(funcs)
|
|
for eq in eqs:
|
|
f1, = eq.lhs.atoms(AppliedUndef)
|
|
f2s = (eq.rhs.atoms(AppliedUndef) - {f1}) & funcset
|
|
eqsmap[f1] = f2s
|
|
eqsorig[f1] = eq
|
|
return eqsmap, eqsorig
|
|
|
|
|
|
def _dict2graph(d):
|
|
nodes = list(d)
|
|
edges = [(f1, f2) for f1, f2s in d.items() for f2 in f2s]
|
|
G = (nodes, edges)
|
|
return G
|
|
|
|
|
|
def _is_type1(scc, t):
|
|
eqs, funcs = scc
|
|
|
|
try:
|
|
(A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, 1)
|
|
except (ODENonlinearError, ODEOrderError):
|
|
return False
|
|
|
|
if _matrix_is_constant(A0, t) and b.is_zero_matrix:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _combine_type1_subsystems(subsystem, funcs, t):
|
|
indices = [i for i, sys in enumerate(zip(subsystem, funcs)) if _is_type1(sys, t)]
|
|
remove = set()
|
|
for ip, i in enumerate(indices):
|
|
for j in indices[ip+1:]:
|
|
if any(eq2.has(funcs[i]) for eq2 in subsystem[j]):
|
|
subsystem[j] = subsystem[i] + subsystem[j]
|
|
remove.add(i)
|
|
subsystem = [sys for i, sys in enumerate(subsystem) if i not in remove]
|
|
return subsystem
|
|
|
|
|
|
def _component_division(eqs, funcs, t):
|
|
|
|
# Assuming that each eq in eqs is in canonical form,
|
|
# that is, [f(x).diff(x) = .., g(x).diff(x) = .., etc]
|
|
# and that the system passed is in its first order
|
|
eqsmap, eqsorig = _eqs2dict(eqs, funcs)
|
|
|
|
subsystems = []
|
|
for cc in connected_components(_dict2graph(eqsmap)):
|
|
eqsmap_c = {f: eqsmap[f] for f in cc}
|
|
sccs = strongly_connected_components(_dict2graph(eqsmap_c))
|
|
subsystem = [[eqsorig[f] for f in scc] for scc in sccs]
|
|
subsystem = _combine_type1_subsystems(subsystem, sccs, t)
|
|
subsystems.append(subsystem)
|
|
|
|
return subsystems
|
|
|
|
|
|
# Returns: List of equations
|
|
def _linear_ode_solver(match):
|
|
t = match['t']
|
|
funcs = match['func']
|
|
|
|
rhs = match.get('rhs', None)
|
|
tau = match.get('tau', None)
|
|
t = match['t_'] if 't_' in match else t
|
|
A = match['func_coeff']
|
|
|
|
# Note: To make B None when the matrix has constant
|
|
# coefficient
|
|
B = match.get('commutative_antiderivative', None)
|
|
type = match['type_of_equation']
|
|
|
|
sol_vector = linodesolve(A, t, b=rhs, B=B,
|
|
type=type, tau=tau)
|
|
|
|
sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)]
|
|
|
|
return sol
|
|
|
|
|
|
def _select_equations(eqs, funcs, key=lambda x: x):
|
|
eq_dict = {e.lhs: e.rhs for e in eqs}
|
|
return [Eq(f, eq_dict[key(f)]) for f in funcs]
|
|
|
|
|
|
def _higher_order_ode_solver(match):
|
|
eqs = match["eq"]
|
|
funcs = match["func"]
|
|
t = match["t"]
|
|
sysorder = match['order']
|
|
type = match.get('type_of_equation', "type0")
|
|
|
|
is_second_order = match.get('is_second_order', False)
|
|
is_transformed = match.get('is_transformed', False)
|
|
is_euler = is_transformed and type == "type1"
|
|
is_higher_order_type2 = is_transformed and type == "type2" and 'P' in match
|
|
|
|
if is_second_order:
|
|
new_eqs, new_funcs = _second_order_to_first_order(eqs, funcs, t,
|
|
A1=match.get("A1", None), A0=match.get("A0", None),
|
|
b=match.get("rhs", None), type=type,
|
|
t_=match.get("t_", None))
|
|
else:
|
|
new_eqs, new_funcs = _higher_order_to_first_order(eqs, sysorder, t, funcs=funcs,
|
|
type=type, J=match.get('J', None),
|
|
f_t=match.get('f(t)', None),
|
|
P=match.get('P', None), b=match.get('rhs', None))
|
|
|
|
if is_transformed:
|
|
t = match.get('t_', t)
|
|
|
|
if not is_higher_order_type2:
|
|
new_eqs = _select_equations(new_eqs, [f.diff(t) for f in new_funcs])
|
|
|
|
sol = None
|
|
|
|
# NotImplementedError may be raised when the system may be actually
|
|
# solvable if it can be just divided into sub-systems
|
|
try:
|
|
if not is_higher_order_type2:
|
|
sol = _strong_component_solver(new_eqs, new_funcs, t)
|
|
except NotImplementedError:
|
|
sol = None
|
|
|
|
# Dividing the system only when it becomes essential
|
|
if sol is None:
|
|
try:
|
|
sol = _component_solver(new_eqs, new_funcs, t)
|
|
except NotImplementedError:
|
|
sol = None
|
|
|
|
if sol is None:
|
|
return sol
|
|
|
|
is_second_order_type2 = is_second_order and type == "type2"
|
|
|
|
underscores = '__' if is_transformed else '_'
|
|
|
|
sol = _select_equations(sol, funcs,
|
|
key=lambda x: Function(Dummy('{}{}0'.format(x.func.__name__, underscores)))(t))
|
|
|
|
if match.get("is_transformed", False):
|
|
if is_second_order_type2:
|
|
g_t = match["g(t)"]
|
|
tau = match["tau"]
|
|
sol = [Eq(s.lhs, s.rhs.subs(t, tau) * g_t) for s in sol]
|
|
elif is_euler:
|
|
t = match['t']
|
|
tau = match['t_']
|
|
sol = [s.subs(tau, log(t)) for s in sol]
|
|
elif is_higher_order_type2:
|
|
P = match['P']
|
|
sol_vector = P * Matrix([s.rhs for s in sol])
|
|
sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)]
|
|
|
|
return sol
|
|
|
|
|
|
# Returns: List of equations or None
|
|
# If None is returned by this solver, then the system
|
|
# of ODEs cannot be solved directly by dsolve_system.
|
|
def _strong_component_solver(eqs, funcs, t):
|
|
from sympy.solvers.ode.ode import dsolve, constant_renumber
|
|
|
|
match = _classify_linear_system(eqs, funcs, t, is_canon=True)
|
|
sol = None
|
|
|
|
# Assuming that we can't get an implicit system
|
|
# since we are already canonical equations from
|
|
# dsolve_system
|
|
if match:
|
|
match['t'] = t
|
|
|
|
if match.get('is_higher_order', False):
|
|
sol = _higher_order_ode_solver(match)
|
|
|
|
elif match.get('is_linear', False):
|
|
sol = _linear_ode_solver(match)
|
|
|
|
# Note: For now, only linear systems are handled by this function
|
|
# hence, the match condition is added. This can be removed later.
|
|
if sol is None and len(eqs) == 1:
|
|
sol = dsolve(eqs[0], func=funcs[0])
|
|
variables = Tuple(eqs[0]).free_symbols
|
|
new_constants = [Dummy() for _ in range(ode_order(eqs[0], funcs[0]))]
|
|
sol = constant_renumber(sol, variables=variables, newconstants=new_constants)
|
|
sol = [sol]
|
|
|
|
# To add non-linear case here in future
|
|
|
|
return sol
|
|
|
|
|
|
def _get_funcs_from_canon(eqs):
|
|
return [eq.lhs.args[0] for eq in eqs]
|
|
|
|
|
|
# Returns: List of Equations(a solution)
|
|
def _weak_component_solver(wcc, t):
|
|
|
|
# We will divide the systems into sccs
|
|
# only when the wcc cannot be solved as
|
|
# a whole
|
|
eqs = []
|
|
for scc in wcc:
|
|
eqs += scc
|
|
funcs = _get_funcs_from_canon(eqs)
|
|
|
|
sol = _strong_component_solver(eqs, funcs, t)
|
|
if sol:
|
|
return sol
|
|
|
|
sol = []
|
|
|
|
for j, scc in enumerate(wcc):
|
|
eqs = scc
|
|
funcs = _get_funcs_from_canon(eqs)
|
|
|
|
# Substituting solutions for the dependent
|
|
# variables solved in previous SCC, if any solved.
|
|
comp_eqs = [eq.subs({s.lhs: s.rhs for s in sol}) for eq in eqs]
|
|
scc_sol = _strong_component_solver(comp_eqs, funcs, t)
|
|
|
|
if scc_sol is None:
|
|
raise NotImplementedError(filldedent('''
|
|
The system of ODEs passed cannot be solved by dsolve_system.
|
|
'''))
|
|
|
|
# scc_sol: List of equations
|
|
# scc_sol is a solution
|
|
sol += scc_sol
|
|
|
|
return sol
|
|
|
|
|
|
# Returns: List of Equations(a solution)
|
|
def _component_solver(eqs, funcs, t):
|
|
components = _component_division(eqs, funcs, t)
|
|
sol = []
|
|
|
|
for wcc in components:
|
|
|
|
# wcc_sol: List of Equations
|
|
sol += _weak_component_solver(wcc, t)
|
|
|
|
# sol: List of Equations
|
|
return sol
|
|
|
|
|
|
def _second_order_to_first_order(eqs, funcs, t, type="auto", A1=None,
|
|
A0=None, b=None, t_=None):
|
|
r"""
|
|
Expects the system to be in second order and in canonical form
|
|
|
|
Explanation
|
|
===========
|
|
|
|
Reduces a second order system into a first order one depending on the type of second
|
|
order system.
|
|
1. "type0": If this is passed, then the system will be reduced to first order by
|
|
introducing dummy variables.
|
|
2. "type1": If this is passed, then a particular substitution will be used to reduce the
|
|
the system into first order.
|
|
3. "type2": If this is passed, then the system will be transformed with new dependent
|
|
variables and independent variables. This transformation is a part of solving
|
|
the corresponding system of ODEs.
|
|
|
|
`A1` and `A0` are the coefficient matrices from the system and it is assumed that the
|
|
second order system has the form given below:
|
|
|
|
.. math::
|
|
A2 * X'' = A1 * X' + A0 * X + b
|
|
|
|
Here, $A2$ is the coefficient matrix for the vector $X''$ and $b$ is the non-homogeneous
|
|
term.
|
|
|
|
Default value for `b` is None but if `A1` and `A0` are passed and `b` is not passed, then the
|
|
system will be assumed homogeneous.
|
|
|
|
"""
|
|
is_a1 = A1 is None
|
|
is_a0 = A0 is None
|
|
|
|
if (type == "type1" and is_a1) or (type == "type2" and is_a0)\
|
|
or (type == "auto" and (is_a1 or is_a0)):
|
|
(A2, A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, 2)
|
|
|
|
if not A2.is_Identity:
|
|
raise ValueError(filldedent('''
|
|
The system must be in its canonical form.
|
|
'''))
|
|
|
|
if type == "auto":
|
|
match = _match_second_order_type(A1, A0, t)
|
|
type = match["type_of_equation"]
|
|
A1 = match.get("A1", None)
|
|
A0 = match.get("A0", None)
|
|
|
|
sys_order = {func: 2 for func in funcs}
|
|
|
|
if type == "type1":
|
|
if b is None:
|
|
b = zeros(len(eqs))
|
|
eqs = _second_order_subs_type1(A1, b, funcs, t)
|
|
sys_order = {func: 1 for func in funcs}
|
|
|
|
if type == "type2":
|
|
if t_ is None:
|
|
t_ = Symbol("{}_".format(t))
|
|
t = t_
|
|
eqs, funcs = _second_order_subs_type2(A0, funcs, t_)
|
|
sys_order = {func: 2 for func in funcs}
|
|
|
|
return _higher_order_to_first_order(eqs, sys_order, t, funcs=funcs)
|
|
|
|
|
|
def _higher_order_type2_to_sub_systems(J, f_t, funcs, t, max_order, b=None, P=None):
|
|
|
|
# Note: To add a test for this ValueError
|
|
if J is None or f_t is None or not _matrix_is_constant(J, t):
|
|
raise ValueError(filldedent('''
|
|
Correctly input for args 'A' and 'f_t' for Linear, Higher Order,
|
|
Type 2
|
|
'''))
|
|
|
|
if P is None and b is not None and not b.is_zero_matrix:
|
|
raise ValueError(filldedent('''
|
|
Provide the keyword 'P' for matrix P in A = P * J * P-1.
|
|
'''))
|
|
|
|
new_funcs = Matrix([Function(Dummy('{}__0'.format(f.func.__name__)))(t) for f in funcs])
|
|
new_eqs = new_funcs.diff(t, max_order) - f_t * J * new_funcs
|
|
|
|
if b is not None and not b.is_zero_matrix:
|
|
new_eqs -= P.inv() * b
|
|
|
|
new_eqs = canonical_odes(new_eqs, new_funcs, t)[0]
|
|
|
|
return new_eqs, new_funcs
|
|
|
|
|
|
def _higher_order_to_first_order(eqs, sys_order, t, funcs=None, type="type0", **kwargs):
|
|
if funcs is None:
|
|
funcs = sys_order.keys()
|
|
|
|
# Standard Cauchy Euler system
|
|
if type == "type1":
|
|
t_ = Symbol('{}_'.format(t))
|
|
new_funcs = [Function(Dummy('{}_'.format(f.func.__name__)))(t_) for f in funcs]
|
|
max_order = max(sys_order[func] for func in funcs)
|
|
subs_dict = {func: new_func for func, new_func in zip(funcs, new_funcs)}
|
|
subs_dict[t] = exp(t_)
|
|
|
|
free_function = Function(Dummy())
|
|
|
|
def _get_coeffs_from_subs_expression(expr):
|
|
if isinstance(expr, Subs):
|
|
free_symbol = expr.args[1][0]
|
|
term = expr.args[0]
|
|
return {ode_order(term, free_symbol): 1}
|
|
|
|
if isinstance(expr, Mul):
|
|
coeff = expr.args[0]
|
|
order = list(_get_coeffs_from_subs_expression(expr.args[1]).keys())[0]
|
|
return {order: coeff}
|
|
|
|
if isinstance(expr, Add):
|
|
coeffs = {}
|
|
for arg in expr.args:
|
|
|
|
if isinstance(arg, Mul):
|
|
coeffs.update(_get_coeffs_from_subs_expression(arg))
|
|
|
|
else:
|
|
order = list(_get_coeffs_from_subs_expression(arg).keys())[0]
|
|
coeffs[order] = 1
|
|
|
|
return coeffs
|
|
|
|
for o in range(1, max_order + 1):
|
|
expr = free_function(log(t_)).diff(t_, o)*t_**o
|
|
coeff_dict = _get_coeffs_from_subs_expression(expr)
|
|
coeffs = [coeff_dict[order] if order in coeff_dict else 0 for order in range(o + 1)]
|
|
expr_to_subs = sum(free_function(t_).diff(t_, i) * c for i, c in
|
|
enumerate(coeffs)) / t**o
|
|
subs_dict.update({f.diff(t, o): expr_to_subs.subs(free_function(t_), nf)
|
|
for f, nf in zip(funcs, new_funcs)})
|
|
|
|
new_eqs = [eq.subs(subs_dict) for eq in eqs]
|
|
new_sys_order = {nf: sys_order[f] for f, nf in zip(funcs, new_funcs)}
|
|
|
|
new_eqs = canonical_odes(new_eqs, new_funcs, t_)[0]
|
|
|
|
return _higher_order_to_first_order(new_eqs, new_sys_order, t_, funcs=new_funcs)
|
|
|
|
# Systems of the form: X(n)(t) = f(t)*A*X + b
|
|
# where X(n)(t) is the nth derivative of the vector of dependent variables
|
|
# with respect to the independent variable and A is a constant matrix.
|
|
if type == "type2":
|
|
J = kwargs.get('J', None)
|
|
f_t = kwargs.get('f_t', None)
|
|
b = kwargs.get('b', None)
|
|
P = kwargs.get('P', None)
|
|
max_order = max(sys_order[func] for func in funcs)
|
|
|
|
return _higher_order_type2_to_sub_systems(J, f_t, funcs, t, max_order, P=P, b=b)
|
|
|
|
# Note: To be changed to this after doit option is disabled for default cases
|
|
# new_sysorder = _get_func_order(new_eqs, new_funcs)
|
|
#
|
|
# return _higher_order_to_first_order(new_eqs, new_sysorder, t, funcs=new_funcs)
|
|
|
|
new_funcs = []
|
|
|
|
for prev_func in funcs:
|
|
func_name = prev_func.func.__name__
|
|
func = Function(Dummy('{}_0'.format(func_name)))(t)
|
|
new_funcs.append(func)
|
|
subs_dict = {prev_func: func}
|
|
new_eqs = []
|
|
|
|
for i in range(1, sys_order[prev_func]):
|
|
new_func = Function(Dummy('{}_{}'.format(func_name, i)))(t)
|
|
subs_dict[prev_func.diff(t, i)] = new_func
|
|
new_funcs.append(new_func)
|
|
|
|
prev_f = subs_dict[prev_func.diff(t, i-1)]
|
|
new_eq = Eq(prev_f.diff(t), new_func)
|
|
new_eqs.append(new_eq)
|
|
|
|
eqs = [eq.subs(subs_dict) for eq in eqs] + new_eqs
|
|
|
|
return eqs, new_funcs
|
|
|
|
|
|
def dsolve_system(eqs, funcs=None, t=None, ics=None, doit=False, simplify=True):
|
|
r"""
|
|
Solves any(supported) system of Ordinary Differential Equations
|
|
|
|
Explanation
|
|
===========
|
|
|
|
This function takes a system of ODEs as an input, determines if the
|
|
it is solvable by this function, and returns the solution if found any.
|
|
|
|
This function can handle:
|
|
1. Linear, First Order, Constant coefficient homogeneous system of ODEs
|
|
2. Linear, First Order, Constant coefficient non-homogeneous system of ODEs
|
|
3. Linear, First Order, non-constant coefficient homogeneous system of ODEs
|
|
4. Linear, First Order, non-constant coefficient non-homogeneous system of ODEs
|
|
5. Any implicit system which can be divided into system of ODEs which is of the above 4 forms
|
|
6. Any higher order linear system of ODEs that can be reduced to one of the 5 forms of systems described above.
|
|
|
|
The types of systems described above are not limited by the number of equations, i.e. this
|
|
function can solve the above types irrespective of the number of equations in the system passed.
|
|
But, the bigger the system, the more time it will take to solve the system.
|
|
|
|
This function returns a list of solutions. Each solution is a list of equations where LHS is
|
|
the dependent variable and RHS is an expression in terms of the independent variable.
|
|
|
|
Among the non constant coefficient types, not all the systems are solvable by this function. Only
|
|
those which have either a coefficient matrix with a commutative antiderivative or those systems which
|
|
may be divided further so that the divided systems may have coefficient matrix with commutative antiderivative.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
eqs : List
|
|
system of ODEs to be solved
|
|
funcs : List or None
|
|
List of dependent variables that make up the system of ODEs
|
|
t : Symbol or None
|
|
Independent variable in the system of ODEs
|
|
ics : Dict or None
|
|
Set of initial boundary/conditions for the system of ODEs
|
|
doit : Boolean
|
|
Evaluate the solutions if True. Default value is True. Can be
|
|
set to false if the integral evaluation takes too much time and/or
|
|
is not required.
|
|
simplify: Boolean
|
|
Simplify the solutions for the systems. Default value is True.
|
|
Can be set to false if simplification takes too much time and/or
|
|
is not required.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import symbols, Eq, Function
|
|
>>> from sympy.solvers.ode.systems import dsolve_system
|
|
>>> f, g = symbols("f g", cls=Function)
|
|
>>> x = symbols("x")
|
|
|
|
>>> eqs = [Eq(f(x).diff(x), g(x)), Eq(g(x).diff(x), f(x))]
|
|
>>> dsolve_system(eqs)
|
|
[[Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))]]
|
|
|
|
You can also pass the initial conditions for the system of ODEs:
|
|
|
|
>>> dsolve_system(eqs, ics={f(0): 1, g(0): 0})
|
|
[[Eq(f(x), exp(x)/2 + exp(-x)/2), Eq(g(x), exp(x)/2 - exp(-x)/2)]]
|
|
|
|
Optionally, you can pass the dependent variables and the independent
|
|
variable for which the system is to be solved:
|
|
|
|
>>> funcs = [f(x), g(x)]
|
|
>>> dsolve_system(eqs, funcs=funcs, t=x)
|
|
[[Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))]]
|
|
|
|
Lets look at an implicit system of ODEs:
|
|
|
|
>>> eqs = [Eq(f(x).diff(x)**2, g(x)**2), Eq(g(x).diff(x), g(x))]
|
|
>>> dsolve_system(eqs)
|
|
[[Eq(f(x), C1 - C2*exp(x)), Eq(g(x), C2*exp(x))], [Eq(f(x), C1 + C2*exp(x)), Eq(g(x), C2*exp(x))]]
|
|
|
|
Returns
|
|
=======
|
|
|
|
List of List of Equations
|
|
|
|
Raises
|
|
======
|
|
|
|
NotImplementedError
|
|
When the system of ODEs is not solvable by this function.
|
|
ValueError
|
|
When the parameters passed are not in the required form.
|
|
|
|
"""
|
|
from sympy.solvers.ode.ode import solve_ics, _extract_funcs, constant_renumber
|
|
|
|
if not iterable(eqs):
|
|
raise ValueError(filldedent('''
|
|
List of equations should be passed. The input is not valid.
|
|
'''))
|
|
|
|
eqs = _preprocess_eqs(eqs)
|
|
|
|
if funcs is not None and not isinstance(funcs, list):
|
|
raise ValueError(filldedent('''
|
|
Input to the funcs should be a list of functions.
|
|
'''))
|
|
|
|
if funcs is None:
|
|
funcs = _extract_funcs(eqs)
|
|
|
|
if any(len(func.args) != 1 for func in funcs):
|
|
raise ValueError(filldedent('''
|
|
dsolve_system can solve a system of ODEs with only one independent
|
|
variable.
|
|
'''))
|
|
|
|
if len(eqs) != len(funcs):
|
|
raise ValueError(filldedent('''
|
|
Number of equations and number of functions do not match
|
|
'''))
|
|
|
|
if t is not None and not isinstance(t, Symbol):
|
|
raise ValueError(filldedent('''
|
|
The independent variable must be of type Symbol
|
|
'''))
|
|
|
|
if t is None:
|
|
t = list(list(eqs[0].atoms(Derivative))[0].atoms(Symbol))[0]
|
|
|
|
sols = []
|
|
canon_eqs = canonical_odes(eqs, funcs, t)
|
|
|
|
for canon_eq in canon_eqs:
|
|
try:
|
|
sol = _strong_component_solver(canon_eq, funcs, t)
|
|
except NotImplementedError:
|
|
sol = None
|
|
|
|
if sol is None:
|
|
sol = _component_solver(canon_eq, funcs, t)
|
|
|
|
sols.append(sol)
|
|
|
|
if sols:
|
|
final_sols = []
|
|
variables = Tuple(*eqs).free_symbols
|
|
|
|
for sol in sols:
|
|
|
|
sol = _select_equations(sol, funcs)
|
|
sol = constant_renumber(sol, variables=variables)
|
|
|
|
if ics:
|
|
constants = Tuple(*sol).free_symbols - variables
|
|
solved_constants = solve_ics(sol, funcs, constants, ics)
|
|
sol = [s.subs(solved_constants) for s in sol]
|
|
|
|
if simplify:
|
|
constants = Tuple(*sol).free_symbols - variables
|
|
sol = simpsol(sol, [t], constants, doit=doit)
|
|
|
|
final_sols.append(sol)
|
|
|
|
sols = final_sols
|
|
|
|
return sols
|