Spaces:
Sleeping
Sleeping
r""" | |
This File contains helper functions for nth_linear_constant_coeff_undetermined_coefficients, | |
nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients, | |
nth_linear_constant_coeff_variation_of_parameters, | |
and nth_linear_euler_eq_nonhomogeneous_variation_of_parameters. | |
All the functions in this file are used by more than one solvers so, instead of creating | |
instances in other classes for using them it is better to keep it here as separate helpers. | |
""" | |
from collections import defaultdict | |
from sympy.core import Add, S | |
from sympy.core.function import diff, expand, _mexpand, expand_mul | |
from sympy.core.relational import Eq | |
from sympy.core.sorting import default_sort_key | |
from sympy.core.symbol import Dummy, Wild | |
from sympy.functions import exp, cos, cosh, im, log, re, sin, sinh, \ | |
atan2, conjugate | |
from sympy.integrals import Integral | |
from sympy.polys import (Poly, RootOf, rootof, roots) | |
from sympy.simplify import collect, simplify, separatevars, powsimp, trigsimp # type: ignore | |
from sympy.utilities import numbered_symbols | |
from sympy.solvers.solvers import solve | |
from sympy.matrices import wronskian | |
from .subscheck import sub_func_doit | |
from sympy.solvers.ode.ode import get_numbered_constants | |
def _test_term(coeff, func, order): | |
r""" | |
Linear Euler ODEs have the form K*x**order*diff(y(x), x, order) = F(x), | |
where K is independent of x and y(x), order>= 0. | |
So we need to check that for each term, coeff == K*x**order from | |
some K. We have a few cases, since coeff may have several | |
different types. | |
""" | |
x = func.args[0] | |
f = func.func | |
if order < 0: | |
raise ValueError("order should be greater than 0") | |
if coeff == 0: | |
return True | |
if order == 0: | |
if x in coeff.free_symbols: | |
return False | |
return True | |
if coeff.is_Mul: | |
if coeff.has(f(x)): | |
return False | |
return x**order in coeff.args | |
elif coeff.is_Pow: | |
return coeff.as_base_exp() == (x, order) | |
elif order == 1: | |
return x == coeff | |
return False | |
def _get_euler_characteristic_eq_sols(eq, func, match_obj): | |
r""" | |
Returns the solution of homogeneous part of the linear euler ODE and | |
the list of roots of characteristic equation. | |
The parameter ``match_obj`` is a dict of order:coeff terms, where order is the order | |
of the derivative on each term, and coeff is the coefficient of that derivative. | |
""" | |
x = func.args[0] | |
f = func.func | |
# First, set up characteristic equation. | |
chareq, symbol = S.Zero, Dummy('x') | |
for i in match_obj: | |
if i >= 0: | |
chareq += (match_obj[i]*diff(x**symbol, x, i)*x**-symbol).expand() | |
chareq = Poly(chareq, symbol) | |
chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] | |
collectterms = [] | |
# A generator of constants | |
constants = list(get_numbered_constants(eq, num=chareq.degree()*2)) | |
constants.reverse() | |
# Create a dict root: multiplicity or charroots | |
charroots = defaultdict(int) | |
for root in chareqroots: | |
charroots[root] += 1 | |
gsol = S.Zero | |
ln = log | |
for root, multiplicity in charroots.items(): | |
for i in range(multiplicity): | |
if isinstance(root, RootOf): | |
gsol += (x**root) * constants.pop() | |
if multiplicity != 1: | |
raise ValueError("Value should be 1") | |
collectterms = [(0, root, 0)] + collectterms | |
elif root.is_real: | |
gsol += ln(x)**i*(x**root) * constants.pop() | |
collectterms = [(i, root, 0)] + collectterms | |
else: | |
reroot = re(root) | |
imroot = im(root) | |
gsol += ln(x)**i * (x**reroot) * ( | |
constants.pop() * sin(abs(imroot)*ln(x)) | |
+ constants.pop() * cos(imroot*ln(x))) | |
collectterms = [(i, reroot, imroot)] + collectterms | |
gsol = Eq(f(x), gsol) | |
gensols = [] | |
# Keep track of when to use sin or cos for nonzero imroot | |
for i, reroot, imroot in collectterms: | |
if imroot == 0: | |
gensols.append(ln(x)**i*x**reroot) | |
else: | |
sin_form = ln(x)**i*x**reroot*sin(abs(imroot)*ln(x)) | |
if sin_form in gensols: | |
cos_form = ln(x)**i*x**reroot*cos(imroot*ln(x)) | |
gensols.append(cos_form) | |
else: | |
gensols.append(sin_form) | |
return gsol, gensols | |
def _solve_variation_of_parameters(eq, func, roots, homogen_sol, order, match_obj, simplify_flag=True): | |
r""" | |
Helper function for the method of variation of parameters and nonhomogeneous euler eq. | |
See the | |
:py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffVariationOfParameters` | |
docstring for more information on this method. | |
The parameter are ``match_obj`` should be a dictionary that has the following | |
keys: | |
``list`` | |
A list of solutions to the homogeneous equation. | |
``sol`` | |
The general solution. | |
""" | |
f = func.func | |
x = func.args[0] | |
r = match_obj | |
psol = 0 | |
wr = wronskian(roots, x) | |
if simplify_flag: | |
wr = simplify(wr) # We need much better simplification for | |
# some ODEs. See issue 4662, for example. | |
# To reduce commonly occurring sin(x)**2 + cos(x)**2 to 1 | |
wr = trigsimp(wr, deep=True, recursive=True) | |
if not wr: | |
# The wronskian will be 0 iff the solutions are not linearly | |
# independent. | |
raise NotImplementedError("Cannot find " + str(order) + | |
" solutions to the homogeneous equation necessary to apply " + | |
"variation of parameters to " + str(eq) + " (Wronskian == 0)") | |
if len(roots) != order: | |
raise NotImplementedError("Cannot find " + str(order) + | |
" solutions to the homogeneous equation necessary to apply " + | |
"variation of parameters to " + | |
str(eq) + " (number of terms != order)") | |
negoneterm = S.NegativeOne**(order) | |
for i in roots: | |
psol += negoneterm*Integral(wronskian([sol for sol in roots if sol != i], x)*r[-1]/wr, x)*i/r[order] | |
negoneterm *= -1 | |
if simplify_flag: | |
psol = simplify(psol) | |
psol = trigsimp(psol, deep=True) | |
return Eq(f(x), homogen_sol.rhs + psol) | |
def _get_const_characteristic_eq_sols(r, func, order): | |
r""" | |
Returns the roots of characteristic equation of constant coefficient | |
linear ODE and list of collectterms which is later on used by simplification | |
to use collect on solution. | |
The parameter `r` is a dict of order:coeff terms, where order is the order of the | |
derivative on each term, and coeff is the coefficient of that derivative. | |
""" | |
x = func.args[0] | |
# First, set up characteristic equation. | |
chareq, symbol = S.Zero, Dummy('x') | |
for i in r.keys(): | |
if isinstance(i, str) or i < 0: | |
pass | |
else: | |
chareq += r[i]*symbol**i | |
chareq = Poly(chareq, symbol) | |
# Can't just call roots because it doesn't return rootof for unsolveable | |
# polynomials. | |
chareqroots = roots(chareq, multiple=True) | |
if len(chareqroots) != order: | |
chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] | |
chareq_is_complex = not all(i.is_real for i in chareq.all_coeffs()) | |
# Create a dict root: multiplicity or charroots | |
charroots = defaultdict(int) | |
for root in chareqroots: | |
charroots[root] += 1 | |
# We need to keep track of terms so we can run collect() at the end. | |
# This is necessary for constantsimp to work properly. | |
collectterms = [] | |
gensols = [] | |
conjugate_roots = [] # used to prevent double-use of conjugate roots | |
# Loop over roots in theorder provided by roots/rootof... | |
for root in chareqroots: | |
# but don't repoeat multiple roots. | |
if root not in charroots: | |
continue | |
multiplicity = charroots.pop(root) | |
for i in range(multiplicity): | |
if chareq_is_complex: | |
gensols.append(x**i*exp(root*x)) | |
collectterms = [(i, root, 0)] + collectterms | |
continue | |
reroot = re(root) | |
imroot = im(root) | |
if imroot.has(atan2) and reroot.has(atan2): | |
# Remove this condition when re and im stop returning | |
# circular atan2 usages. | |
gensols.append(x**i*exp(root*x)) | |
collectterms = [(i, root, 0)] + collectterms | |
else: | |
if root in conjugate_roots: | |
collectterms = [(i, reroot, imroot)] + collectterms | |
continue | |
if imroot == 0: | |
gensols.append(x**i*exp(reroot*x)) | |
collectterms = [(i, reroot, 0)] + collectterms | |
continue | |
conjugate_roots.append(conjugate(root)) | |
gensols.append(x**i*exp(reroot*x) * sin(abs(imroot) * x)) | |
gensols.append(x**i*exp(reroot*x) * cos( imroot * x)) | |
# This ordering is important | |
collectterms = [(i, reroot, imroot)] + collectterms | |
return gensols, collectterms | |
# Ideally these kind of simplification functions shouldn't be part of solvers. | |
# odesimp should be improved to handle these kind of specific simplifications. | |
def _get_simplified_sol(sol, func, collectterms): | |
r""" | |
Helper function which collects the solution on | |
collectterms. Ideally this should be handled by odesimp.It is used | |
only when the simplify is set to True in dsolve. | |
The parameter ``collectterms`` is a list of tuple (i, reroot, imroot) where `i` is | |
the multiplicity of the root, reroot is real part and imroot being the imaginary part. | |
""" | |
f = func.func | |
x = func.args[0] | |
collectterms.sort(key=default_sort_key) | |
collectterms.reverse() | |
assert len(sol) == 1 and sol[0].lhs == f(x) | |
sol = sol[0].rhs | |
sol = expand_mul(sol) | |
for i, reroot, imroot in collectterms: | |
sol = collect(sol, x**i*exp(reroot*x)*sin(abs(imroot)*x)) | |
sol = collect(sol, x**i*exp(reroot*x)*cos(imroot*x)) | |
for i, reroot, imroot in collectterms: | |
sol = collect(sol, x**i*exp(reroot*x)) | |
sol = powsimp(sol) | |
return Eq(f(x), sol) | |
def _undetermined_coefficients_match(expr, x, func=None, eq_homogeneous=S.Zero): | |
r""" | |
Returns a trial function match if undetermined coefficients can be applied | |
to ``expr``, and ``None`` otherwise. | |
A trial expression can be found for an expression for use with the method | |
of undetermined coefficients if the expression is an | |
additive/multiplicative combination of constants, polynomials in `x` (the | |
independent variable of expr), `\sin(a x + b)`, `\cos(a x + b)`, and | |
`e^{a x}` terms (in other words, it has a finite number of linearly | |
independent derivatives). | |
Note that you may still need to multiply each term returned here by | |
sufficient `x` to make it linearly independent with the solutions to the | |
homogeneous equation. | |
This is intended for internal use by ``undetermined_coefficients`` hints. | |
SymPy currently has no way to convert `\sin^n(x) \cos^m(y)` into a sum of | |
only `\sin(a x)` and `\cos(b x)` terms, so these are not implemented. So, | |
for example, you will need to manually convert `\sin^2(x)` into `[1 + | |
\cos(2 x)]/2` to properly apply the method of undetermined coefficients on | |
it. | |
Examples | |
======== | |
>>> from sympy import log, exp | |
>>> from sympy.solvers.ode.nonhomogeneous import _undetermined_coefficients_match | |
>>> from sympy.abc import x | |
>>> _undetermined_coefficients_match(9*x*exp(x) + exp(-x), x) | |
{'test': True, 'trialset': {x*exp(x), exp(-x), exp(x)}} | |
>>> _undetermined_coefficients_match(log(x), x) | |
{'test': False} | |
""" | |
a = Wild('a', exclude=[x]) | |
b = Wild('b', exclude=[x]) | |
expr = powsimp(expr, combine='exp') # exp(x)*exp(2*x + 1) => exp(3*x + 1) | |
retdict = {} | |
def _test_term(expr, x): | |
r""" | |
Test if ``expr`` fits the proper form for undetermined coefficients. | |
""" | |
if not expr.has(x): | |
return True | |
elif expr.is_Add: | |
return all(_test_term(i, x) for i in expr.args) | |
elif expr.is_Mul: | |
if expr.has(sin, cos): | |
foundtrig = False | |
# Make sure that there is only one trig function in the args. | |
# See the docstring. | |
for i in expr.args: | |
if i.has(sin, cos): | |
if foundtrig: | |
return False | |
else: | |
foundtrig = True | |
return all(_test_term(i, x) for i in expr.args) | |
elif expr.is_Function: | |
if expr.func in (sin, cos, exp, sinh, cosh): | |
if expr.args[0].match(a*x + b): | |
return True | |
else: | |
return False | |
else: | |
return False | |
elif expr.is_Pow and expr.base.is_Symbol and expr.exp.is_Integer and \ | |
expr.exp >= 0: | |
return True | |
elif expr.is_Pow and expr.base.is_number: | |
if expr.exp.match(a*x + b): | |
return True | |
else: | |
return False | |
elif expr.is_Symbol or expr.is_number: | |
return True | |
else: | |
return False | |
def _get_trial_set(expr, x, exprs=set()): | |
r""" | |
Returns a set of trial terms for undetermined coefficients. | |
The idea behind undetermined coefficients is that the terms expression | |
repeat themselves after a finite number of derivatives, except for the | |
coefficients (they are linearly dependent). So if we collect these, | |
we should have the terms of our trial function. | |
""" | |
def _remove_coefficient(expr, x): | |
r""" | |
Returns the expression without a coefficient. | |
Similar to expr.as_independent(x)[1], except it only works | |
multiplicatively. | |
""" | |
term = S.One | |
if expr.is_Mul: | |
for i in expr.args: | |
if i.has(x): | |
term *= i | |
elif expr.has(x): | |
term = expr | |
return term | |
expr = expand_mul(expr) | |
if expr.is_Add: | |
for term in expr.args: | |
if _remove_coefficient(term, x) in exprs: | |
pass | |
else: | |
exprs.add(_remove_coefficient(term, x)) | |
exprs = exprs.union(_get_trial_set(term, x, exprs)) | |
else: | |
term = _remove_coefficient(expr, x) | |
tmpset = exprs.union({term}) | |
oldset = set() | |
while tmpset != oldset: | |
# If you get stuck in this loop, then _test_term is probably | |
# broken | |
oldset = tmpset.copy() | |
expr = expr.diff(x) | |
term = _remove_coefficient(expr, x) | |
if term.is_Add: | |
tmpset = tmpset.union(_get_trial_set(term, x, tmpset)) | |
else: | |
tmpset.add(term) | |
exprs = tmpset | |
return exprs | |
def is_homogeneous_solution(term): | |
r""" This function checks whether the given trialset contains any root | |
of homogeneous equation""" | |
return expand(sub_func_doit(eq_homogeneous, func, term)).is_zero | |
retdict['test'] = _test_term(expr, x) | |
if retdict['test']: | |
# Try to generate a list of trial solutions that will have the | |
# undetermined coefficients. Note that if any of these are not linearly | |
# independent with any of the solutions to the homogeneous equation, | |
# then they will need to be multiplied by sufficient x to make them so. | |
# This function DOES NOT do that (it doesn't even look at the | |
# homogeneous equation). | |
temp_set = set() | |
for i in Add.make_args(expr): | |
act = _get_trial_set(i, x) | |
if eq_homogeneous is not S.Zero: | |
while any(is_homogeneous_solution(ts) for ts in act): | |
act = {x*ts for ts in act} | |
temp_set = temp_set.union(act) | |
retdict['trialset'] = temp_set | |
return retdict | |
def _solve_undetermined_coefficients(eq, func, order, match, trialset): | |
r""" | |
Helper function for the method of undetermined coefficients. | |
See the | |
:py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffUndeterminedCoefficients` | |
docstring for more information on this method. | |
The parameter ``trialset`` is the set of trial functions as returned by | |
``_undetermined_coefficients_match()['trialset']``. | |
The parameter ``match`` should be a dictionary that has the following | |
keys: | |
``list`` | |
A list of solutions to the homogeneous equation. | |
``sol`` | |
The general solution. | |
""" | |
r = match | |
coeffs = numbered_symbols('a', cls=Dummy) | |
coefflist = [] | |
gensols = r['list'] | |
gsol = r['sol'] | |
f = func.func | |
x = func.args[0] | |
if len(gensols) != order: | |
raise NotImplementedError("Cannot find " + str(order) + | |
" solutions to the homogeneous equation necessary to apply" + | |
" undetermined coefficients to " + str(eq) + | |
" (number of terms != order)") | |
trialfunc = 0 | |
for i in trialset: | |
c = next(coeffs) | |
coefflist.append(c) | |
trialfunc += c*i | |
eqs = sub_func_doit(eq, f(x), trialfunc) | |
coeffsdict = dict(list(zip(trialset, [0]*(len(trialset) + 1)))) | |
eqs = _mexpand(eqs) | |
for i in Add.make_args(eqs): | |
s = separatevars(i, dict=True, symbols=[x]) | |
if coeffsdict.get(s[x]): | |
coeffsdict[s[x]] += s['coeff'] | |
else: | |
coeffsdict[s[x]] = s['coeff'] | |
coeffvals = solve(list(coeffsdict.values()), coefflist) | |
if not coeffvals: | |
raise NotImplementedError( | |
"Could not solve `%s` using the " | |
"method of undetermined coefficients " | |
"(unable to solve for coefficients)." % eq) | |
psol = trialfunc.subs(coeffvals) | |
return Eq(f(x), gsol.rhs + psol) | |