nonhomogeneous.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. r"""
  2. This File contains helper functions for nth_linear_constant_coeff_undetermined_coefficients,
  3. nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients,
  4. nth_linear_constant_coeff_variation_of_parameters,
  5. and nth_linear_euler_eq_nonhomogeneous_variation_of_parameters.
  6. All the functions in this file are used by more than one solvers so, instead of creating
  7. instances in other classes for using them it is better to keep it here as separate helpers.
  8. """
  9. from collections import defaultdict
  10. from sympy.core import Add, S
  11. from sympy.core.function import diff, expand, _mexpand, expand_mul
  12. from sympy.core.relational import Eq
  13. from sympy.core.sorting import default_sort_key
  14. from sympy.core.symbol import Dummy, Wild
  15. from sympy.functions import exp, cos, cosh, im, log, re, sin, sinh, \
  16. atan2, conjugate
  17. from sympy.integrals import Integral
  18. from sympy.polys import (Poly, RootOf, rootof, roots)
  19. from sympy.simplify import collect, simplify, separatevars, powsimp, trigsimp # type: ignore
  20. from sympy.utilities import numbered_symbols
  21. from sympy.solvers.solvers import solve
  22. from sympy.matrices import wronskian
  23. from .subscheck import sub_func_doit
  24. from sympy.solvers.ode.ode import get_numbered_constants
  25. def _test_term(coeff, func, order):
  26. r"""
  27. Linear Euler ODEs have the form K*x**order*diff(y(x), x, order) = F(x),
  28. where K is independent of x and y(x), order>= 0.
  29. So we need to check that for each term, coeff == K*x**order from
  30. some K. We have a few cases, since coeff may have several
  31. different types.
  32. """
  33. x = func.args[0]
  34. f = func.func
  35. if order < 0:
  36. raise ValueError("order should be greater than 0")
  37. if coeff == 0:
  38. return True
  39. if order == 0:
  40. if x in coeff.free_symbols:
  41. return False
  42. return True
  43. if coeff.is_Mul:
  44. if coeff.has(f(x)):
  45. return False
  46. return x**order in coeff.args
  47. elif coeff.is_Pow:
  48. return coeff.as_base_exp() == (x, order)
  49. elif order == 1:
  50. return x == coeff
  51. return False
  52. def _get_euler_characteristic_eq_sols(eq, func, match_obj):
  53. r"""
  54. Returns the solution of homogeneous part of the linear euler ODE and
  55. the list of roots of characteristic equation.
  56. The parameter ``match_obj`` is a dict of order:coeff terms, where order is the order
  57. of the derivative on each term, and coeff is the coefficient of that derivative.
  58. """
  59. x = func.args[0]
  60. f = func.func
  61. # First, set up characteristic equation.
  62. chareq, symbol = S.Zero, Dummy('x')
  63. for i in match_obj:
  64. if i >= 0:
  65. chareq += (match_obj[i]*diff(x**symbol, x, i)*x**-symbol).expand()
  66. chareq = Poly(chareq, symbol)
  67. chareqroots = [rootof(chareq, k) for k in range(chareq.degree())]
  68. collectterms = []
  69. # A generator of constants
  70. constants = list(get_numbered_constants(eq, num=chareq.degree()*2))
  71. constants.reverse()
  72. # Create a dict root: multiplicity or charroots
  73. charroots = defaultdict(int)
  74. for root in chareqroots:
  75. charroots[root] += 1
  76. gsol = S.Zero
  77. ln = log
  78. for root, multiplicity in charroots.items():
  79. for i in range(multiplicity):
  80. if isinstance(root, RootOf):
  81. gsol += (x**root) * constants.pop()
  82. if multiplicity != 1:
  83. raise ValueError("Value should be 1")
  84. collectterms = [(0, root, 0)] + collectterms
  85. elif root.is_real:
  86. gsol += ln(x)**i*(x**root) * constants.pop()
  87. collectterms = [(i, root, 0)] + collectterms
  88. else:
  89. reroot = re(root)
  90. imroot = im(root)
  91. gsol += ln(x)**i * (x**reroot) * (
  92. constants.pop() * sin(abs(imroot)*ln(x))
  93. + constants.pop() * cos(imroot*ln(x)))
  94. collectterms = [(i, reroot, imroot)] + collectterms
  95. gsol = Eq(f(x), gsol)
  96. gensols = []
  97. # Keep track of when to use sin or cos for nonzero imroot
  98. for i, reroot, imroot in collectterms:
  99. if imroot == 0:
  100. gensols.append(ln(x)**i*x**reroot)
  101. else:
  102. sin_form = ln(x)**i*x**reroot*sin(abs(imroot)*ln(x))
  103. if sin_form in gensols:
  104. cos_form = ln(x)**i*x**reroot*cos(imroot*ln(x))
  105. gensols.append(cos_form)
  106. else:
  107. gensols.append(sin_form)
  108. return gsol, gensols
  109. def _solve_variation_of_parameters(eq, func, roots, homogen_sol, order, match_obj, simplify_flag=True):
  110. r"""
  111. Helper function for the method of variation of parameters and nonhomogeneous euler eq.
  112. See the
  113. :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffVariationOfParameters`
  114. docstring for more information on this method.
  115. The parameter are ``match_obj`` should be a dictionary that has the following
  116. keys:
  117. ``list``
  118. A list of solutions to the homogeneous equation.
  119. ``sol``
  120. The general solution.
  121. """
  122. f = func.func
  123. x = func.args[0]
  124. r = match_obj
  125. psol = 0
  126. wr = wronskian(roots, x)
  127. if simplify_flag:
  128. wr = simplify(wr) # We need much better simplification for
  129. # some ODEs. See issue 4662, for example.
  130. # To reduce commonly occurring sin(x)**2 + cos(x)**2 to 1
  131. wr = trigsimp(wr, deep=True, recursive=True)
  132. if not wr:
  133. # The wronskian will be 0 iff the solutions are not linearly
  134. # independent.
  135. raise NotImplementedError("Cannot find " + str(order) +
  136. " solutions to the homogeneous equation necessary to apply " +
  137. "variation of parameters to " + str(eq) + " (Wronskian == 0)")
  138. if len(roots) != order:
  139. raise NotImplementedError("Cannot find " + str(order) +
  140. " solutions to the homogeneous equation necessary to apply " +
  141. "variation of parameters to " +
  142. str(eq) + " (number of terms != order)")
  143. negoneterm = S.NegativeOne**(order)
  144. for i in roots:
  145. psol += negoneterm*Integral(wronskian([sol for sol in roots if sol != i], x)*r[-1]/wr, x)*i/r[order]
  146. negoneterm *= -1
  147. if simplify_flag:
  148. psol = simplify(psol)
  149. psol = trigsimp(psol, deep=True)
  150. return Eq(f(x), homogen_sol.rhs + psol)
  151. def _get_const_characteristic_eq_sols(r, func, order):
  152. r"""
  153. Returns the roots of characteristic equation of constant coefficient
  154. linear ODE and list of collectterms which is later on used by simplification
  155. to use collect on solution.
  156. The parameter `r` is a dict of order:coeff terms, where order is the order of the
  157. derivative on each term, and coeff is the coefficient of that derivative.
  158. """
  159. x = func.args[0]
  160. # First, set up characteristic equation.
  161. chareq, symbol = S.Zero, Dummy('x')
  162. for i in r.keys():
  163. if isinstance(i, str) or i < 0:
  164. pass
  165. else:
  166. chareq += r[i]*symbol**i
  167. chareq = Poly(chareq, symbol)
  168. # Can't just call roots because it doesn't return rootof for unsolveable
  169. # polynomials.
  170. chareqroots = roots(chareq, multiple=True)
  171. if len(chareqroots) != order:
  172. chareqroots = [rootof(chareq, k) for k in range(chareq.degree())]
  173. chareq_is_complex = not all(i.is_real for i in chareq.all_coeffs())
  174. # Create a dict root: multiplicity or charroots
  175. charroots = defaultdict(int)
  176. for root in chareqroots:
  177. charroots[root] += 1
  178. # We need to keep track of terms so we can run collect() at the end.
  179. # This is necessary for constantsimp to work properly.
  180. collectterms = []
  181. gensols = []
  182. conjugate_roots = [] # used to prevent double-use of conjugate roots
  183. # Loop over roots in theorder provided by roots/rootof...
  184. for root in chareqroots:
  185. # but don't repoeat multiple roots.
  186. if root not in charroots:
  187. continue
  188. multiplicity = charroots.pop(root)
  189. for i in range(multiplicity):
  190. if chareq_is_complex:
  191. gensols.append(x**i*exp(root*x))
  192. collectterms = [(i, root, 0)] + collectterms
  193. continue
  194. reroot = re(root)
  195. imroot = im(root)
  196. if imroot.has(atan2) and reroot.has(atan2):
  197. # Remove this condition when re and im stop returning
  198. # circular atan2 usages.
  199. gensols.append(x**i*exp(root*x))
  200. collectterms = [(i, root, 0)] + collectterms
  201. else:
  202. if root in conjugate_roots:
  203. collectterms = [(i, reroot, imroot)] + collectterms
  204. continue
  205. if imroot == 0:
  206. gensols.append(x**i*exp(reroot*x))
  207. collectterms = [(i, reroot, 0)] + collectterms
  208. continue
  209. conjugate_roots.append(conjugate(root))
  210. gensols.append(x**i*exp(reroot*x) * sin(abs(imroot) * x))
  211. gensols.append(x**i*exp(reroot*x) * cos( imroot * x))
  212. # This ordering is important
  213. collectterms = [(i, reroot, imroot)] + collectterms
  214. return gensols, collectterms
  215. # Ideally these kind of simplification functions shouldn't be part of solvers.
  216. # odesimp should be improved to handle these kind of specific simplifications.
  217. def _get_simplified_sol(sol, func, collectterms):
  218. r"""
  219. Helper function which collects the solution on
  220. collectterms. Ideally this should be handled by odesimp.It is used
  221. only when the simplify is set to True in dsolve.
  222. The parameter ``collectterms`` is a list of tuple (i, reroot, imroot) where `i` is
  223. the multiplicity of the root, reroot is real part and imroot being the imaginary part.
  224. """
  225. f = func.func
  226. x = func.args[0]
  227. collectterms.sort(key=default_sort_key)
  228. collectterms.reverse()
  229. assert len(sol) == 1 and sol[0].lhs == f(x)
  230. sol = sol[0].rhs
  231. sol = expand_mul(sol)
  232. for i, reroot, imroot in collectterms:
  233. sol = collect(sol, x**i*exp(reroot*x)*sin(abs(imroot)*x))
  234. sol = collect(sol, x**i*exp(reroot*x)*cos(imroot*x))
  235. for i, reroot, imroot in collectterms:
  236. sol = collect(sol, x**i*exp(reroot*x))
  237. sol = powsimp(sol)
  238. return Eq(f(x), sol)
  239. def _undetermined_coefficients_match(expr, x, func=None, eq_homogeneous=S.Zero):
  240. r"""
  241. Returns a trial function match if undetermined coefficients can be applied
  242. to ``expr``, and ``None`` otherwise.
  243. A trial expression can be found for an expression for use with the method
  244. of undetermined coefficients if the expression is an
  245. additive/multiplicative combination of constants, polynomials in `x` (the
  246. independent variable of expr), `\sin(a x + b)`, `\cos(a x + b)`, and
  247. `e^{a x}` terms (in other words, it has a finite number of linearly
  248. independent derivatives).
  249. Note that you may still need to multiply each term returned here by
  250. sufficient `x` to make it linearly independent with the solutions to the
  251. homogeneous equation.
  252. This is intended for internal use by ``undetermined_coefficients`` hints.
  253. SymPy currently has no way to convert `\sin^n(x) \cos^m(y)` into a sum of
  254. only `\sin(a x)` and `\cos(b x)` terms, so these are not implemented. So,
  255. for example, you will need to manually convert `\sin^2(x)` into `[1 +
  256. \cos(2 x)]/2` to properly apply the method of undetermined coefficients on
  257. it.
  258. Examples
  259. ========
  260. >>> from sympy import log, exp
  261. >>> from sympy.solvers.ode.nonhomogeneous import _undetermined_coefficients_match
  262. >>> from sympy.abc import x
  263. >>> _undetermined_coefficients_match(9*x*exp(x) + exp(-x), x)
  264. {'test': True, 'trialset': {x*exp(x), exp(-x), exp(x)}}
  265. >>> _undetermined_coefficients_match(log(x), x)
  266. {'test': False}
  267. """
  268. a = Wild('a', exclude=[x])
  269. b = Wild('b', exclude=[x])
  270. expr = powsimp(expr, combine='exp') # exp(x)*exp(2*x + 1) => exp(3*x + 1)
  271. retdict = {}
  272. def _test_term(expr, x):
  273. r"""
  274. Test if ``expr`` fits the proper form for undetermined coefficients.
  275. """
  276. if not expr.has(x):
  277. return True
  278. elif expr.is_Add:
  279. return all(_test_term(i, x) for i in expr.args)
  280. elif expr.is_Mul:
  281. if expr.has(sin, cos):
  282. foundtrig = False
  283. # Make sure that there is only one trig function in the args.
  284. # See the docstring.
  285. for i in expr.args:
  286. if i.has(sin, cos):
  287. if foundtrig:
  288. return False
  289. else:
  290. foundtrig = True
  291. return all(_test_term(i, x) for i in expr.args)
  292. elif expr.is_Function:
  293. if expr.func in (sin, cos, exp, sinh, cosh):
  294. if expr.args[0].match(a*x + b):
  295. return True
  296. else:
  297. return False
  298. else:
  299. return False
  300. elif expr.is_Pow and expr.base.is_Symbol and expr.exp.is_Integer and \
  301. expr.exp >= 0:
  302. return True
  303. elif expr.is_Pow and expr.base.is_number:
  304. if expr.exp.match(a*x + b):
  305. return True
  306. else:
  307. return False
  308. elif expr.is_Symbol or expr.is_number:
  309. return True
  310. else:
  311. return False
  312. def _get_trial_set(expr, x, exprs=set()):
  313. r"""
  314. Returns a set of trial terms for undetermined coefficients.
  315. The idea behind undetermined coefficients is that the terms expression
  316. repeat themselves after a finite number of derivatives, except for the
  317. coefficients (they are linearly dependent). So if we collect these,
  318. we should have the terms of our trial function.
  319. """
  320. def _remove_coefficient(expr, x):
  321. r"""
  322. Returns the expression without a coefficient.
  323. Similar to expr.as_independent(x)[1], except it only works
  324. multiplicatively.
  325. """
  326. term = S.One
  327. if expr.is_Mul:
  328. for i in expr.args:
  329. if i.has(x):
  330. term *= i
  331. elif expr.has(x):
  332. term = expr
  333. return term
  334. expr = expand_mul(expr)
  335. if expr.is_Add:
  336. for term in expr.args:
  337. if _remove_coefficient(term, x) in exprs:
  338. pass
  339. else:
  340. exprs.add(_remove_coefficient(term, x))
  341. exprs = exprs.union(_get_trial_set(term, x, exprs))
  342. else:
  343. term = _remove_coefficient(expr, x)
  344. tmpset = exprs.union({term})
  345. oldset = set()
  346. while tmpset != oldset:
  347. # If you get stuck in this loop, then _test_term is probably
  348. # broken
  349. oldset = tmpset.copy()
  350. expr = expr.diff(x)
  351. term = _remove_coefficient(expr, x)
  352. if term.is_Add:
  353. tmpset = tmpset.union(_get_trial_set(term, x, tmpset))
  354. else:
  355. tmpset.add(term)
  356. exprs = tmpset
  357. return exprs
  358. def is_homogeneous_solution(term):
  359. r""" This function checks whether the given trialset contains any root
  360. of homogenous equation"""
  361. return expand(sub_func_doit(eq_homogeneous, func, term)).is_zero
  362. retdict['test'] = _test_term(expr, x)
  363. if retdict['test']:
  364. # Try to generate a list of trial solutions that will have the
  365. # undetermined coefficients. Note that if any of these are not linearly
  366. # independent with any of the solutions to the homogeneous equation,
  367. # then they will need to be multiplied by sufficient x to make them so.
  368. # This function DOES NOT do that (it doesn't even look at the
  369. # homogeneous equation).
  370. temp_set = set()
  371. for i in Add.make_args(expr):
  372. act = _get_trial_set(i, x)
  373. if eq_homogeneous is not S.Zero:
  374. while any(is_homogeneous_solution(ts) for ts in act):
  375. act = {x*ts for ts in act}
  376. temp_set = temp_set.union(act)
  377. retdict['trialset'] = temp_set
  378. return retdict
  379. def _solve_undetermined_coefficients(eq, func, order, match, trialset):
  380. r"""
  381. Helper function for the method of undetermined coefficients.
  382. See the
  383. :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffUndeterminedCoefficients`
  384. docstring for more information on this method.
  385. The parameter ``trialset`` is the set of trial functions as returned by
  386. ``_undetermined_coefficients_match()['trialset']``.
  387. The parameter ``match`` should be a dictionary that has the following
  388. keys:
  389. ``list``
  390. A list of solutions to the homogeneous equation.
  391. ``sol``
  392. The general solution.
  393. """
  394. r = match
  395. coeffs = numbered_symbols('a', cls=Dummy)
  396. coefflist = []
  397. gensols = r['list']
  398. gsol = r['sol']
  399. f = func.func
  400. x = func.args[0]
  401. if len(gensols) != order:
  402. raise NotImplementedError("Cannot find " + str(order) +
  403. " solutions to the homogeneous equation necessary to apply" +
  404. " undetermined coefficients to " + str(eq) +
  405. " (number of terms != order)")
  406. trialfunc = 0
  407. for i in trialset:
  408. c = next(coeffs)
  409. coefflist.append(c)
  410. trialfunc += c*i
  411. eqs = sub_func_doit(eq, f(x), trialfunc)
  412. coeffsdict = dict(list(zip(trialset, [0]*(len(trialset) + 1))))
  413. eqs = _mexpand(eqs)
  414. for i in Add.make_args(eqs):
  415. s = separatevars(i, dict=True, symbols=[x])
  416. if coeffsdict.get(s[x]):
  417. coeffsdict[s[x]] += s['coeff']
  418. else:
  419. coeffsdict[s[x]] = s['coeff']
  420. coeffvals = solve(list(coeffsdict.values()), coefflist)
  421. if not coeffvals:
  422. raise NotImplementedError(
  423. "Could not solve `%s` using the "
  424. "method of undetermined coefficients "
  425. "(unable to solve for coefficients)." % eq)
  426. psol = trialfunc.subs(coeffvals)
  427. return Eq(f(x), gsol.rhs + psol)