Coverage for /usr/lib/python3/dist-packages/sympy/solvers/ode/nonhomogeneous.py: 8%

261 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1r""" 

2This File contains helper functions for nth_linear_constant_coeff_undetermined_coefficients, 

3nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients, 

4nth_linear_constant_coeff_variation_of_parameters, 

5and nth_linear_euler_eq_nonhomogeneous_variation_of_parameters. 

6 

7All the functions in this file are used by more than one solvers so, instead of creating 

8instances in other classes for using them it is better to keep it here as separate helpers. 

9 

10""" 

11from collections import defaultdict 

12from sympy.core import Add, S 

13from sympy.core.function import diff, expand, _mexpand, expand_mul 

14from sympy.core.relational import Eq 

15from sympy.core.sorting import default_sort_key 

16from sympy.core.symbol import Dummy, Wild 

17from sympy.functions import exp, cos, cosh, im, log, re, sin, sinh, \ 

18 atan2, conjugate 

19from sympy.integrals import Integral 

20from sympy.polys import (Poly, RootOf, rootof, roots) 

21from sympy.simplify import collect, simplify, separatevars, powsimp, trigsimp # type: ignore 

22from sympy.utilities import numbered_symbols 

23from sympy.solvers.solvers import solve 

24from sympy.matrices import wronskian 

25from .subscheck import sub_func_doit 

26from sympy.solvers.ode.ode import get_numbered_constants 

27 

28 

29def _test_term(coeff, func, order): 

30 r""" 

31 Linear Euler ODEs have the form K*x**order*diff(y(x), x, order) = F(x), 

32 where K is independent of x and y(x), order>= 0. 

33 So we need to check that for each term, coeff == K*x**order from 

34 some K. We have a few cases, since coeff may have several 

35 different types. 

36 """ 

37 x = func.args[0] 

38 f = func.func 

39 if order < 0: 

40 raise ValueError("order should be greater than 0") 

41 if coeff == 0: 

42 return True 

43 if order == 0: 

44 if x in coeff.free_symbols: 

45 return False 

46 return True 

47 if coeff.is_Mul: 

48 if coeff.has(f(x)): 

49 return False 

50 return x**order in coeff.args 

51 elif coeff.is_Pow: 

52 return coeff.as_base_exp() == (x, order) 

53 elif order == 1: 

54 return x == coeff 

55 return False 

56 

57 

58def _get_euler_characteristic_eq_sols(eq, func, match_obj): 

59 r""" 

60 Returns the solution of homogeneous part of the linear euler ODE and 

61 the list of roots of characteristic equation. 

62 

63 The parameter ``match_obj`` is a dict of order:coeff terms, where order is the order 

64 of the derivative on each term, and coeff is the coefficient of that derivative. 

65 

66 """ 

67 x = func.args[0] 

68 f = func.func 

69 

70 # First, set up characteristic equation. 

71 chareq, symbol = S.Zero, Dummy('x') 

72 

73 for i in match_obj: 

74 if i >= 0: 

75 chareq += (match_obj[i]*diff(x**symbol, x, i)*x**-symbol).expand() 

76 

77 chareq = Poly(chareq, symbol) 

78 chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] 

79 collectterms = [] 

80 

81 # A generator of constants 

82 constants = list(get_numbered_constants(eq, num=chareq.degree()*2)) 

83 constants.reverse() 

84 

85 # Create a dict root: multiplicity or charroots 

86 charroots = defaultdict(int) 

87 for root in chareqroots: 

88 charroots[root] += 1 

89 gsol = S.Zero 

90 ln = log 

91 for root, multiplicity in charroots.items(): 

92 for i in range(multiplicity): 

93 if isinstance(root, RootOf): 

94 gsol += (x**root) * constants.pop() 

95 if multiplicity != 1: 

96 raise ValueError("Value should be 1") 

97 collectterms = [(0, root, 0)] + collectterms 

98 elif root.is_real: 

99 gsol += ln(x)**i*(x**root) * constants.pop() 

100 collectterms = [(i, root, 0)] + collectterms 

101 else: 

102 reroot = re(root) 

103 imroot = im(root) 

104 gsol += ln(x)**i * (x**reroot) * ( 

105 constants.pop() * sin(abs(imroot)*ln(x)) 

106 + constants.pop() * cos(imroot*ln(x))) 

107 collectterms = [(i, reroot, imroot)] + collectterms 

108 

109 gsol = Eq(f(x), gsol) 

110 

111 gensols = [] 

112 # Keep track of when to use sin or cos for nonzero imroot 

113 for i, reroot, imroot in collectterms: 

114 if imroot == 0: 

115 gensols.append(ln(x)**i*x**reroot) 

116 else: 

117 sin_form = ln(x)**i*x**reroot*sin(abs(imroot)*ln(x)) 

118 if sin_form in gensols: 

119 cos_form = ln(x)**i*x**reroot*cos(imroot*ln(x)) 

120 gensols.append(cos_form) 

121 else: 

122 gensols.append(sin_form) 

123 return gsol, gensols 

124 

125 

126def _solve_variation_of_parameters(eq, func, roots, homogen_sol, order, match_obj, simplify_flag=True): 

127 r""" 

128 Helper function for the method of variation of parameters and nonhomogeneous euler eq. 

129 

130 See the 

131 :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffVariationOfParameters` 

132 docstring for more information on this method. 

133 

134 The parameter are ``match_obj`` should be a dictionary that has the following 

135 keys: 

136 

137 ``list`` 

138 A list of solutions to the homogeneous equation. 

139 

140 ``sol`` 

141 The general solution. 

142 

143 """ 

144 f = func.func 

145 x = func.args[0] 

146 r = match_obj 

147 psol = 0 

148 wr = wronskian(roots, x) 

149 

150 if simplify_flag: 

151 wr = simplify(wr) # We need much better simplification for 

152 # some ODEs. See issue 4662, for example. 

153 # To reduce commonly occurring sin(x)**2 + cos(x)**2 to 1 

154 wr = trigsimp(wr, deep=True, recursive=True) 

155 if not wr: 

156 # The wronskian will be 0 iff the solutions are not linearly 

157 # independent. 

158 raise NotImplementedError("Cannot find " + str(order) + 

159 " solutions to the homogeneous equation necessary to apply " + 

160 "variation of parameters to " + str(eq) + " (Wronskian == 0)") 

161 if len(roots) != order: 

162 raise NotImplementedError("Cannot find " + str(order) + 

163 " solutions to the homogeneous equation necessary to apply " + 

164 "variation of parameters to " + 

165 str(eq) + " (number of terms != order)") 

166 negoneterm = S.NegativeOne**(order) 

167 for i in roots: 

168 psol += negoneterm*Integral(wronskian([sol for sol in roots if sol != i], x)*r[-1]/wr, x)*i/r[order] 

169 negoneterm *= -1 

170 

171 if simplify_flag: 

172 psol = simplify(psol) 

173 psol = trigsimp(psol, deep=True) 

174 return Eq(f(x), homogen_sol.rhs + psol) 

175 

176 

177def _get_const_characteristic_eq_sols(r, func, order): 

178 r""" 

179 Returns the roots of characteristic equation of constant coefficient 

180 linear ODE and list of collectterms which is later on used by simplification 

181 to use collect on solution. 

182 

183 The parameter `r` is a dict of order:coeff terms, where order is the order of the 

184 derivative on each term, and coeff is the coefficient of that derivative. 

185 

186 """ 

187 x = func.args[0] 

188 # First, set up characteristic equation. 

189 chareq, symbol = S.Zero, Dummy('x') 

190 

191 for i in r.keys(): 

192 if isinstance(i, str) or i < 0: 

193 pass 

194 else: 

195 chareq += r[i]*symbol**i 

196 

197 chareq = Poly(chareq, symbol) 

198 # Can't just call roots because it doesn't return rootof for unsolveable 

199 # polynomials. 

200 chareqroots = roots(chareq, multiple=True) 

201 if len(chareqroots) != order: 

202 chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] 

203 

204 chareq_is_complex = not all(i.is_real for i in chareq.all_coeffs()) 

205 

206 # Create a dict root: multiplicity or charroots 

207 charroots = defaultdict(int) 

208 for root in chareqroots: 

209 charroots[root] += 1 

210 # We need to keep track of terms so we can run collect() at the end. 

211 # This is necessary for constantsimp to work properly. 

212 collectterms = [] 

213 gensols = [] 

214 conjugate_roots = [] # used to prevent double-use of conjugate roots 

215 # Loop over roots in theorder provided by roots/rootof... 

216 for root in chareqroots: 

217 # but don't repoeat multiple roots. 

218 if root not in charroots: 

219 continue 

220 multiplicity = charroots.pop(root) 

221 for i in range(multiplicity): 

222 if chareq_is_complex: 

223 gensols.append(x**i*exp(root*x)) 

224 collectterms = [(i, root, 0)] + collectterms 

225 continue 

226 reroot = re(root) 

227 imroot = im(root) 

228 if imroot.has(atan2) and reroot.has(atan2): 

229 # Remove this condition when re and im stop returning 

230 # circular atan2 usages. 

231 gensols.append(x**i*exp(root*x)) 

232 collectterms = [(i, root, 0)] + collectterms 

233 else: 

234 if root in conjugate_roots: 

235 collectterms = [(i, reroot, imroot)] + collectterms 

236 continue 

237 if imroot == 0: 

238 gensols.append(x**i*exp(reroot*x)) 

239 collectterms = [(i, reroot, 0)] + collectterms 

240 continue 

241 conjugate_roots.append(conjugate(root)) 

242 gensols.append(x**i*exp(reroot*x) * sin(abs(imroot) * x)) 

243 gensols.append(x**i*exp(reroot*x) * cos( imroot * x)) 

244 

245 # This ordering is important 

246 collectterms = [(i, reroot, imroot)] + collectterms 

247 return gensols, collectterms 

248 

249 

250# Ideally these kind of simplification functions shouldn't be part of solvers. 

251# odesimp should be improved to handle these kind of specific simplifications. 

252def _get_simplified_sol(sol, func, collectterms): 

253 r""" 

254 Helper function which collects the solution on 

255 collectterms. Ideally this should be handled by odesimp.It is used 

256 only when the simplify is set to True in dsolve. 

257 

258 The parameter ``collectterms`` is a list of tuple (i, reroot, imroot) where `i` is 

259 the multiplicity of the root, reroot is real part and imroot being the imaginary part. 

260 

261 """ 

262 f = func.func 

263 x = func.args[0] 

264 collectterms.sort(key=default_sort_key) 

265 collectterms.reverse() 

266 assert len(sol) == 1 and sol[0].lhs == f(x) 

267 sol = sol[0].rhs 

268 sol = expand_mul(sol) 

269 for i, reroot, imroot in collectterms: 

270 sol = collect(sol, x**i*exp(reroot*x)*sin(abs(imroot)*x)) 

271 sol = collect(sol, x**i*exp(reroot*x)*cos(imroot*x)) 

272 for i, reroot, imroot in collectterms: 

273 sol = collect(sol, x**i*exp(reroot*x)) 

274 sol = powsimp(sol) 

275 return Eq(f(x), sol) 

276 

277 

278def _undetermined_coefficients_match(expr, x, func=None, eq_homogeneous=S.Zero): 

279 r""" 

280 Returns a trial function match if undetermined coefficients can be applied 

281 to ``expr``, and ``None`` otherwise. 

282 

283 A trial expression can be found for an expression for use with the method 

284 of undetermined coefficients if the expression is an 

285 additive/multiplicative combination of constants, polynomials in `x` (the 

286 independent variable of expr), `\sin(a x + b)`, `\cos(a x + b)`, and 

287 `e^{a x}` terms (in other words, it has a finite number of linearly 

288 independent derivatives). 

289 

290 Note that you may still need to multiply each term returned here by 

291 sufficient `x` to make it linearly independent with the solutions to the 

292 homogeneous equation. 

293 

294 This is intended for internal use by ``undetermined_coefficients`` hints. 

295 

296 SymPy currently has no way to convert `\sin^n(x) \cos^m(y)` into a sum of 

297 only `\sin(a x)` and `\cos(b x)` terms, so these are not implemented. So, 

298 for example, you will need to manually convert `\sin^2(x)` into `[1 + 

299 \cos(2 x)]/2` to properly apply the method of undetermined coefficients on 

300 it. 

301 

302 Examples 

303 ======== 

304 

305 >>> from sympy import log, exp 

306 >>> from sympy.solvers.ode.nonhomogeneous import _undetermined_coefficients_match 

307 >>> from sympy.abc import x 

308 >>> _undetermined_coefficients_match(9*x*exp(x) + exp(-x), x) 

309 {'test': True, 'trialset': {x*exp(x), exp(-x), exp(x)}} 

310 >>> _undetermined_coefficients_match(log(x), x) 

311 {'test': False} 

312 

313 """ 

314 a = Wild('a', exclude=[x]) 

315 b = Wild('b', exclude=[x]) 

316 expr = powsimp(expr, combine='exp') # exp(x)*exp(2*x + 1) => exp(3*x + 1) 

317 retdict = {} 

318 

319 def _test_term(expr, x): 

320 r""" 

321 Test if ``expr`` fits the proper form for undetermined coefficients. 

322 """ 

323 if not expr.has(x): 

324 return True 

325 elif expr.is_Add: 

326 return all(_test_term(i, x) for i in expr.args) 

327 elif expr.is_Mul: 

328 if expr.has(sin, cos): 

329 foundtrig = False 

330 # Make sure that there is only one trig function in the args. 

331 # See the docstring. 

332 for i in expr.args: 

333 if i.has(sin, cos): 

334 if foundtrig: 

335 return False 

336 else: 

337 foundtrig = True 

338 return all(_test_term(i, x) for i in expr.args) 

339 elif expr.is_Function: 

340 if expr.func in (sin, cos, exp, sinh, cosh): 

341 if expr.args[0].match(a*x + b): 

342 return True 

343 else: 

344 return False 

345 else: 

346 return False 

347 elif expr.is_Pow and expr.base.is_Symbol and expr.exp.is_Integer and \ 

348 expr.exp >= 0: 

349 return True 

350 elif expr.is_Pow and expr.base.is_number: 

351 if expr.exp.match(a*x + b): 

352 return True 

353 else: 

354 return False 

355 elif expr.is_Symbol or expr.is_number: 

356 return True 

357 else: 

358 return False 

359 

360 def _get_trial_set(expr, x, exprs=set()): 

361 r""" 

362 Returns a set of trial terms for undetermined coefficients. 

363 

364 The idea behind undetermined coefficients is that the terms expression 

365 repeat themselves after a finite number of derivatives, except for the 

366 coefficients (they are linearly dependent). So if we collect these, 

367 we should have the terms of our trial function. 

368 """ 

369 def _remove_coefficient(expr, x): 

370 r""" 

371 Returns the expression without a coefficient. 

372 

373 Similar to expr.as_independent(x)[1], except it only works 

374 multiplicatively. 

375 """ 

376 term = S.One 

377 if expr.is_Mul: 

378 for i in expr.args: 

379 if i.has(x): 

380 term *= i 

381 elif expr.has(x): 

382 term = expr 

383 return term 

384 

385 expr = expand_mul(expr) 

386 if expr.is_Add: 

387 for term in expr.args: 

388 if _remove_coefficient(term, x) in exprs: 

389 pass 

390 else: 

391 exprs.add(_remove_coefficient(term, x)) 

392 exprs = exprs.union(_get_trial_set(term, x, exprs)) 

393 else: 

394 term = _remove_coefficient(expr, x) 

395 tmpset = exprs.union({term}) 

396 oldset = set() 

397 while tmpset != oldset: 

398 # If you get stuck in this loop, then _test_term is probably 

399 # broken 

400 oldset = tmpset.copy() 

401 expr = expr.diff(x) 

402 term = _remove_coefficient(expr, x) 

403 if term.is_Add: 

404 tmpset = tmpset.union(_get_trial_set(term, x, tmpset)) 

405 else: 

406 tmpset.add(term) 

407 exprs = tmpset 

408 return exprs 

409 

410 def is_homogeneous_solution(term): 

411 r""" This function checks whether the given trialset contains any root 

412 of homogeneous equation""" 

413 return expand(sub_func_doit(eq_homogeneous, func, term)).is_zero 

414 

415 retdict['test'] = _test_term(expr, x) 

416 if retdict['test']: 

417 # Try to generate a list of trial solutions that will have the 

418 # undetermined coefficients. Note that if any of these are not linearly 

419 # independent with any of the solutions to the homogeneous equation, 

420 # then they will need to be multiplied by sufficient x to make them so. 

421 # This function DOES NOT do that (it doesn't even look at the 

422 # homogeneous equation). 

423 temp_set = set() 

424 for i in Add.make_args(expr): 

425 act = _get_trial_set(i, x) 

426 if eq_homogeneous is not S.Zero: 

427 while any(is_homogeneous_solution(ts) for ts in act): 

428 act = {x*ts for ts in act} 

429 temp_set = temp_set.union(act) 

430 

431 retdict['trialset'] = temp_set 

432 return retdict 

433 

434 

435def _solve_undetermined_coefficients(eq, func, order, match, trialset): 

436 r""" 

437 Helper function for the method of undetermined coefficients. 

438 

439 See the 

440 :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffUndeterminedCoefficients` 

441 docstring for more information on this method. 

442 

443 The parameter ``trialset`` is the set of trial functions as returned by 

444 ``_undetermined_coefficients_match()['trialset']``. 

445 

446 The parameter ``match`` should be a dictionary that has the following 

447 keys: 

448 

449 ``list`` 

450 A list of solutions to the homogeneous equation. 

451 

452 ``sol`` 

453 The general solution. 

454 

455 """ 

456 r = match 

457 coeffs = numbered_symbols('a', cls=Dummy) 

458 coefflist = [] 

459 gensols = r['list'] 

460 gsol = r['sol'] 

461 f = func.func 

462 x = func.args[0] 

463 

464 if len(gensols) != order: 

465 raise NotImplementedError("Cannot find " + str(order) + 

466 " solutions to the homogeneous equation necessary to apply" + 

467 " undetermined coefficients to " + str(eq) + 

468 " (number of terms != order)") 

469 

470 trialfunc = 0 

471 for i in trialset: 

472 c = next(coeffs) 

473 coefflist.append(c) 

474 trialfunc += c*i 

475 

476 eqs = sub_func_doit(eq, f(x), trialfunc) 

477 

478 coeffsdict = dict(list(zip(trialset, [0]*(len(trialset) + 1)))) 

479 

480 eqs = _mexpand(eqs) 

481 

482 for i in Add.make_args(eqs): 

483 s = separatevars(i, dict=True, symbols=[x]) 

484 if coeffsdict.get(s[x]): 

485 coeffsdict[s[x]] += s['coeff'] 

486 else: 

487 coeffsdict[s[x]] = s['coeff'] 

488 

489 coeffvals = solve(list(coeffsdict.values()), coefflist) 

490 

491 if not coeffvals: 

492 raise NotImplementedError( 

493 "Could not solve `%s` using the " 

494 "method of undetermined coefficients " 

495 "(unable to solve for coefficients)." % eq) 

496 

497 psol = trialfunc.subs(coeffvals) 

498 

499 return Eq(f(x), gsol.rhs + psol)