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

179 statements  

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

1from sympy.core import S, Pow 

2from sympy.core.function import (Derivative, AppliedUndef, diff) 

3from sympy.core.relational import Equality, Eq 

4from sympy.core.symbol import Dummy 

5from sympy.core.sympify import sympify 

6 

7from sympy.logic.boolalg import BooleanAtom 

8from sympy.functions import exp 

9from sympy.series import Order 

10from sympy.simplify.simplify import simplify, posify, besselsimp 

11from sympy.simplify.trigsimp import trigsimp 

12from sympy.simplify.sqrtdenest import sqrtdenest 

13from sympy.solvers import solve 

14from sympy.solvers.deutils import _preprocess, ode_order 

15from sympy.utilities.iterables import iterable, is_sequence 

16 

17 

18def sub_func_doit(eq, func, new): 

19 r""" 

20 When replacing the func with something else, we usually want the 

21 derivative evaluated, so this function helps in making that happen. 

22 

23 Examples 

24 ======== 

25 

26 >>> from sympy import Derivative, symbols, Function 

27 >>> from sympy.solvers.ode.subscheck import sub_func_doit 

28 >>> x, z = symbols('x, z') 

29 >>> y = Function('y') 

30 

31 >>> sub_func_doit(3*Derivative(y(x), x) - 1, y(x), x) 

32 2 

33 

34 >>> sub_func_doit(x*Derivative(y(x), x) - y(x)**2 + y(x), y(x), 

35 ... 1/(x*(z + 1/x))) 

36 x*(-1/(x**2*(z + 1/x)) + 1/(x**3*(z + 1/x)**2)) + 1/(x*(z + 1/x)) 

37 ...- 1/(x**2*(z + 1/x)**2) 

38 """ 

39 reps= {func: new} 

40 for d in eq.atoms(Derivative): 

41 if d.expr == func: 

42 reps[d] = new.diff(*d.variable_count) 

43 else: 

44 reps[d] = d.xreplace({func: new}).doit(deep=False) 

45 return eq.xreplace(reps) 

46 

47 

48def checkodesol(ode, sol, func=None, order='auto', solve_for_func=True): 

49 r""" 

50 Substitutes ``sol`` into ``ode`` and checks that the result is ``0``. 

51 

52 This works when ``func`` is one function, like `f(x)` or a list of 

53 functions like `[f(x), g(x)]` when `ode` is a system of ODEs. ``sol`` can 

54 be a single solution or a list of solutions. Each solution may be an 

55 :py:class:`~sympy.core.relational.Equality` that the solution satisfies, 

56 e.g. ``Eq(f(x), C1), Eq(f(x) + C1, 0)``; or simply an 

57 :py:class:`~sympy.core.expr.Expr`, e.g. ``f(x) - C1``. In most cases it 

58 will not be necessary to explicitly identify the function, but if the 

59 function cannot be inferred from the original equation it can be supplied 

60 through the ``func`` argument. 

61 

62 If a sequence of solutions is passed, the same sort of container will be 

63 used to return the result for each solution. 

64 

65 It tries the following methods, in order, until it finds zero equivalence: 

66 

67 1. Substitute the solution for `f` in the original equation. This only 

68 works if ``ode`` is solved for `f`. It will attempt to solve it first 

69 unless ``solve_for_func == False``. 

70 2. Take `n` derivatives of the solution, where `n` is the order of 

71 ``ode``, and check to see if that is equal to the solution. This only 

72 works on exact ODEs. 

73 3. Take the 1st, 2nd, ..., `n`\th derivatives of the solution, each time 

74 solving for the derivative of `f` of that order (this will always be 

75 possible because `f` is a linear operator). Then back substitute each 

76 derivative into ``ode`` in reverse order. 

77 

78 This function returns a tuple. The first item in the tuple is ``True`` if 

79 the substitution results in ``0``, and ``False`` otherwise. The second 

80 item in the tuple is what the substitution results in. It should always 

81 be ``0`` if the first item is ``True``. Sometimes this function will 

82 return ``False`` even when an expression is identically equal to ``0``. 

83 This happens when :py:meth:`~sympy.simplify.simplify.simplify` does not 

84 reduce the expression to ``0``. If an expression returned by this 

85 function vanishes identically, then ``sol`` really is a solution to 

86 the ``ode``. 

87 

88 If this function seems to hang, it is probably because of a hard 

89 simplification. 

90 

91 To use this function to test, test the first item of the tuple. 

92 

93 Examples 

94 ======== 

95 

96 >>> from sympy import (Eq, Function, checkodesol, symbols, 

97 ... Derivative, exp) 

98 >>> x, C1, C2 = symbols('x,C1,C2') 

99 >>> f, g = symbols('f g', cls=Function) 

100 >>> checkodesol(f(x).diff(x), Eq(f(x), C1)) 

101 (True, 0) 

102 >>> assert checkodesol(f(x).diff(x), C1)[0] 

103 >>> assert not checkodesol(f(x).diff(x), x)[0] 

104 >>> checkodesol(f(x).diff(x, 2), x**2) 

105 (False, 2) 

106 

107 >>> eqs = [Eq(Derivative(f(x), x), f(x)), Eq(Derivative(g(x), x), g(x))] 

108 >>> sol = [Eq(f(x), C1*exp(x)), Eq(g(x), C2*exp(x))] 

109 >>> checkodesol(eqs, sol) 

110 (True, [0, 0]) 

111 

112 """ 

113 if iterable(ode): 

114 return checksysodesol(ode, sol, func=func) 

115 

116 if not isinstance(ode, Equality): 

117 ode = Eq(ode, 0) 

118 if func is None: 

119 try: 

120 _, func = _preprocess(ode.lhs) 

121 except ValueError: 

122 funcs = [s.atoms(AppliedUndef) for s in ( 

123 sol if is_sequence(sol, set) else [sol])] 

124 funcs = set().union(*funcs) 

125 if len(funcs) != 1: 

126 raise ValueError( 

127 'must pass func arg to checkodesol for this case.') 

128 func = funcs.pop() 

129 if not isinstance(func, AppliedUndef) or len(func.args) != 1: 

130 raise ValueError( 

131 "func must be a function of one variable, not %s" % func) 

132 if is_sequence(sol, set): 

133 return type(sol)([checkodesol(ode, i, order=order, solve_for_func=solve_for_func) for i in sol]) 

134 

135 if not isinstance(sol, Equality): 

136 sol = Eq(func, sol) 

137 elif sol.rhs == func: 

138 sol = sol.reversed 

139 

140 if order == 'auto': 

141 order = ode_order(ode, func) 

142 solved = sol.lhs == func and not sol.rhs.has(func) 

143 if solve_for_func and not solved: 

144 rhs = solve(sol, func) 

145 if rhs: 

146 eqs = [Eq(func, t) for t in rhs] 

147 if len(rhs) == 1: 

148 eqs = eqs[0] 

149 return checkodesol(ode, eqs, order=order, 

150 solve_for_func=False) 

151 

152 x = func.args[0] 

153 

154 # Handle series solutions here 

155 if sol.has(Order): 

156 assert sol.lhs == func 

157 Oterm = sol.rhs.getO() 

158 solrhs = sol.rhs.removeO() 

159 

160 Oexpr = Oterm.expr 

161 assert isinstance(Oexpr, Pow) 

162 sorder = Oexpr.exp 

163 assert Oterm == Order(x**sorder) 

164 

165 odesubs = (ode.lhs-ode.rhs).subs(func, solrhs).doit().expand() 

166 

167 neworder = Order(x**(sorder - order)) 

168 odesubs = odesubs + neworder 

169 assert odesubs.getO() == neworder 

170 residual = odesubs.removeO() 

171 

172 return (residual == 0, residual) 

173 

174 s = True 

175 testnum = 0 

176 while s: 

177 if testnum == 0: 

178 # First pass, try substituting a solved solution directly into the 

179 # ODE. This has the highest chance of succeeding. 

180 ode_diff = ode.lhs - ode.rhs 

181 

182 if sol.lhs == func: 

183 s = sub_func_doit(ode_diff, func, sol.rhs) 

184 s = besselsimp(s) 

185 else: 

186 testnum += 1 

187 continue 

188 ss = simplify(s.rewrite(exp)) 

189 if ss: 

190 # with the new numer_denom in power.py, if we do a simple 

191 # expansion then testnum == 0 verifies all solutions. 

192 s = ss.expand(force=True) 

193 else: 

194 s = 0 

195 testnum += 1 

196 elif testnum == 1: 

197 # Second pass. If we cannot substitute f, try seeing if the nth 

198 # derivative is equal, this will only work for odes that are exact, 

199 # by definition. 

200 s = simplify( 

201 trigsimp(diff(sol.lhs, x, order) - diff(sol.rhs, x, order)) - 

202 trigsimp(ode.lhs) + trigsimp(ode.rhs)) 

203 # s2 = simplify( 

204 # diff(sol.lhs, x, order) - diff(sol.rhs, x, order) - \ 

205 # ode.lhs + ode.rhs) 

206 testnum += 1 

207 elif testnum == 2: 

208 # Third pass. Try solving for df/dx and substituting that into the 

209 # ODE. Thanks to Chris Smith for suggesting this method. Many of 

210 # the comments below are his, too. 

211 # The method: 

212 # - Take each of 1..n derivatives of the solution. 

213 # - Solve each nth derivative for d^(n)f/dx^(n) 

214 # (the differential of that order) 

215 # - Back substitute into the ODE in decreasing order 

216 # (i.e., n, n-1, ...) 

217 # - Check the result for zero equivalence 

218 if sol.lhs == func and not sol.rhs.has(func): 

219 diffsols = {0: sol.rhs} 

220 elif sol.rhs == func and not sol.lhs.has(func): 

221 diffsols = {0: sol.lhs} 

222 else: 

223 diffsols = {} 

224 sol = sol.lhs - sol.rhs 

225 for i in range(1, order + 1): 

226 # Differentiation is a linear operator, so there should always 

227 # be 1 solution. Nonetheless, we test just to make sure. 

228 # We only need to solve once. After that, we automatically 

229 # have the solution to the differential in the order we want. 

230 if i == 1: 

231 ds = sol.diff(x) 

232 try: 

233 sdf = solve(ds, func.diff(x, i)) 

234 if not sdf: 

235 raise NotImplementedError 

236 except NotImplementedError: 

237 testnum += 1 

238 break 

239 else: 

240 diffsols[i] = sdf[0] 

241 else: 

242 # This is what the solution says df/dx should be. 

243 diffsols[i] = diffsols[i - 1].diff(x) 

244 

245 # Make sure the above didn't fail. 

246 if testnum > 2: 

247 continue 

248 else: 

249 # Substitute it into ODE to check for self consistency. 

250 lhs, rhs = ode.lhs, ode.rhs 

251 for i in range(order, -1, -1): 

252 if i == 0 and 0 not in diffsols: 

253 # We can only substitute f(x) if the solution was 

254 # solved for f(x). 

255 break 

256 lhs = sub_func_doit(lhs, func.diff(x, i), diffsols[i]) 

257 rhs = sub_func_doit(rhs, func.diff(x, i), diffsols[i]) 

258 ode_or_bool = Eq(lhs, rhs) 

259 ode_or_bool = simplify(ode_or_bool) 

260 

261 if isinstance(ode_or_bool, (bool, BooleanAtom)): 

262 if ode_or_bool: 

263 lhs = rhs = S.Zero 

264 else: 

265 lhs = ode_or_bool.lhs 

266 rhs = ode_or_bool.rhs 

267 # No sense in overworking simplify -- just prove that the 

268 # numerator goes to zero 

269 num = trigsimp((lhs - rhs).as_numer_denom()[0]) 

270 # since solutions are obtained using force=True we test 

271 # using the same level of assumptions 

272 ## replace function with dummy so assumptions will work 

273 _func = Dummy('func') 

274 num = num.subs(func, _func) 

275 ## posify the expression 

276 num, reps = posify(num) 

277 s = simplify(num).xreplace(reps).xreplace({_func: func}) 

278 testnum += 1 

279 else: 

280 break 

281 

282 if not s: 

283 return (True, s) 

284 elif s is True: # The code above never was able to change s 

285 raise NotImplementedError("Unable to test if " + str(sol) + 

286 " is a solution to " + str(ode) + ".") 

287 else: 

288 return (False, s) 

289 

290 

291def checksysodesol(eqs, sols, func=None): 

292 r""" 

293 Substitutes corresponding ``sols`` for each functions into each ``eqs`` and 

294 checks that the result of substitutions for each equation is ``0``. The 

295 equations and solutions passed can be any iterable. 

296 

297 This only works when each ``sols`` have one function only, like `x(t)` or `y(t)`. 

298 For each function, ``sols`` can have a single solution or a list of solutions. 

299 In most cases it will not be necessary to explicitly identify the function, 

300 but if the function cannot be inferred from the original equation it 

301 can be supplied through the ``func`` argument. 

302 

303 When a sequence of equations is passed, the same sequence is used to return 

304 the result for each equation with each function substituted with corresponding 

305 solutions. 

306 

307 It tries the following method to find zero equivalence for each equation: 

308 

309 Substitute the solutions for functions, like `x(t)` and `y(t)` into the 

310 original equations containing those functions. 

311 This function returns a tuple. The first item in the tuple is ``True`` if 

312 the substitution results for each equation is ``0``, and ``False`` otherwise. 

313 The second item in the tuple is what the substitution results in. Each element 

314 of the ``list`` should always be ``0`` corresponding to each equation if the 

315 first item is ``True``. Note that sometimes this function may return ``False``, 

316 but with an expression that is identically equal to ``0``, instead of returning 

317 ``True``. This is because :py:meth:`~sympy.simplify.simplify.simplify` cannot 

318 reduce the expression to ``0``. If an expression returned by each function 

319 vanishes identically, then ``sols`` really is a solution to ``eqs``. 

320 

321 If this function seems to hang, it is probably because of a difficult simplification. 

322 

323 Examples 

324 ======== 

325 

326 >>> from sympy import Eq, diff, symbols, sin, cos, exp, sqrt, S, Function 

327 >>> from sympy.solvers.ode.subscheck import checksysodesol 

328 >>> C1, C2 = symbols('C1:3') 

329 >>> t = symbols('t') 

330 >>> x, y = symbols('x, y', cls=Function) 

331 >>> eq = (Eq(diff(x(t),t), x(t) + y(t) + 17), Eq(diff(y(t),t), -2*x(t) + y(t) + 12)) 

332 >>> sol = [Eq(x(t), (C1*sin(sqrt(2)*t) + C2*cos(sqrt(2)*t))*exp(t) - S(5)/3), 

333 ... Eq(y(t), (sqrt(2)*C1*cos(sqrt(2)*t) - sqrt(2)*C2*sin(sqrt(2)*t))*exp(t) - S(46)/3)] 

334 >>> checksysodesol(eq, sol) 

335 (True, [0, 0]) 

336 >>> eq = (Eq(diff(x(t),t),x(t)*y(t)**4), Eq(diff(y(t),t),y(t)**3)) 

337 >>> sol = [Eq(x(t), C1*exp(-1/(4*(C2 + t)))), Eq(y(t), -sqrt(2)*sqrt(-1/(C2 + t))/2), 

338 ... Eq(x(t), C1*exp(-1/(4*(C2 + t)))), Eq(y(t), sqrt(2)*sqrt(-1/(C2 + t))/2)] 

339 >>> checksysodesol(eq, sol) 

340 (True, [0, 0]) 

341 

342 """ 

343 def _sympify(eq): 

344 return list(map(sympify, eq if iterable(eq) else [eq])) 

345 eqs = _sympify(eqs) 

346 for i in range(len(eqs)): 

347 if isinstance(eqs[i], Equality): 

348 eqs[i] = eqs[i].lhs - eqs[i].rhs 

349 if func is None: 

350 funcs = [] 

351 for eq in eqs: 

352 derivs = eq.atoms(Derivative) 

353 func = set().union(*[d.atoms(AppliedUndef) for d in derivs]) 

354 funcs.extend(func) 

355 funcs = list(set(funcs)) 

356 if not all(isinstance(func, AppliedUndef) and len(func.args) == 1 for func in funcs)\ 

357 and len({func.args for func in funcs})!=1: 

358 raise ValueError("func must be a function of one variable, not %s" % func) 

359 for sol in sols: 

360 if len(sol.atoms(AppliedUndef)) != 1: 

361 raise ValueError("solutions should have one function only") 

362 if len(funcs) != len({sol.lhs for sol in sols}): 

363 raise ValueError("number of solutions provided does not match the number of equations") 

364 dictsol = {} 

365 for sol in sols: 

366 func = list(sol.atoms(AppliedUndef))[0] 

367 if sol.rhs == func: 

368 sol = sol.reversed 

369 solved = sol.lhs == func and not sol.rhs.has(func) 

370 if not solved: 

371 rhs = solve(sol, func) 

372 if not rhs: 

373 raise NotImplementedError 

374 else: 

375 rhs = sol.rhs 

376 dictsol[func] = rhs 

377 checkeq = [] 

378 for eq in eqs: 

379 for func in funcs: 

380 eq = sub_func_doit(eq, func, dictsol[func]) 

381 ss = simplify(eq) 

382 if ss != 0: 

383 eq = ss.expand(force=True) 

384 if eq != 0: 

385 eq = sqrtdenest(eq).simplify() 

386 else: 

387 eq = 0 

388 checkeq.append(eq) 

389 if len(set(checkeq)) == 1 and list(set(checkeq))[0] == 0: 

390 return (True, checkeq) 

391 else: 

392 return (False, checkeq)