(solving-guide-ode)= # Solve an Ordinary Differential Equation (ODE) Algebraically Use SymPy to solve an ordinary differential equation (ODE) algebraically. For example, solving $y''(x) + 9y(x)=0 $ yields $ y(x)=C_{1} \sin(3x)+ C_{2} \cos(3x)$. ## Alternatives to Consider - To numerically solve a system of ODEs, use a [SciPy ODE solver](https://docs.scipy.org/doc/scipy/reference/integrate.html#solving-initial-value-problems-for-ode-systems) such as `solve_ivp`. You can also use SymPy to create and then {func}`~.lambdify` an ODE to be solved numerically using SciPy's as `solve_ivp` as described below in [](#numerically-solve-an-ode-in-scipy). ## Solve an Ordinary Differential Equation (ODE) Here is an example of solving the above ordinary differential equation algebraically using {func}`~.dsolve`. You can then use {func}`~.checkodesol` to verify that the solution is correct. ```py >>> from sympy import Function, dsolve, Derivative, checkodesol >>> from sympy.abc import x >>> y = Function('y') >>> # Solve the ODE >>> result = dsolve(Derivative(y(x), x, x) + 9*y(x), y(x)) >>> result Eq(y(x), C1*sin(3*x) + C2*cos(3*x)) >>> # Check that the solution is correct >>> checkodesol(Derivative(y(x), x, x) + 9*y(x), result) (True, 0) ``` The output of {func}`~.checkodesol` is a tuple where the first item, a boolean, tells whether substituting the solution into the ODE results in `0`, indicating the solution is correct. ## Guidance ### Defining Derivatives There are many ways to express derivatives of functions. For an undefined function, both {class}`~.Derivative` and {func}`~.diff` represent the undefined derivative. Thus, all of the following `ypp` ("y prime prime") represent $y''$, the second derivative with respect to $x$ of a function $y(x)$: ```py ypp = y(x).diff(x, x) ypp = y(x).diff(x, 2) ypp = y(x).diff((x, 2)) ypp = diff(y(x), x, x) ypp = diff(y(x), x, 2) ypp = Derivative(y(x), x, x) ypp = Derivative(y(x), x, 2) ypp = Derivative(Derivative(y(x), x), x) ypp = diff(diff(y(x), x), x) yp = y(x).diff(x) ypp = yp.diff(x) ``` We recommend specifying the function to be solved for, as the second argument to {func}`~.dsolve`. Note that it must be a function rather than a variable (symbol). SymPy will give an error if you specify a variable ($x$) rather than a function ($f(x)$): ```py >>> dsolve(Derivative(y(x), x, x) + 9*y(x), x) Traceback (most recent call last): ... ValueError: dsolve() and classify_ode() only work with functions of one variable, not x ``` Similarly, you must specify the argument of the function: $y(x)$, not just $y$. ## Options to Define an ODE You can define the function to be solved for in two ways. The subsequent syntax for specifying initial conditions depends on your choice. ### Option 1: Define a Function Without Including Its Independent Variable You can define a function without including its independent variable: ```py >>> from sympy import symbols, Eq, Function, dsolve >>> 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(eqs, [f(x), g(x)]) [Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))] ``` Note that you supply the functions to be solved for as a list as the second argument of {func}`~.dsolve`, here `[f(x), g(x)]`. #### Specify Initial Conditions or Boundary Conditions If your differential equation(s) have initial or boundary conditions, specify them with the {func}`~.dsolve` optional argument `ics`. Initial and boundary conditions are treated the same way (even though the argument is called `ics`). It should be given in the form of `{f(x0): y0, f(x).diff(x).subs(x, x1): y1}` and so on where, for example, the value of $f(x)$ at $x = x_{0}$ is $y_{0}$. For power series solutions, if no initial conditions are specified $f(0)$ is assumed to be $C_{0}$ and the power series solution is calculated about $0$. Here is an example of setting the initial values for functions, namely namely $f(0) = 1$ and $g(2) = 3$: ```py >>> from sympy import symbols, Eq, Function, dsolve >>> 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(eqs, [f(x), g(x)]) [Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))] >>> dsolve(eqs, [f(x), g(x)], ics={f(0): 1, g(2): 3}) [Eq(f(x), (1 + 3*exp(2))*exp(x)/(1 + exp(4)) - (-exp(4) + 3*exp(2))*exp(-x)/(1 + exp(4))), Eq(g(x), (1 + 3*exp(2))*exp(x)/(1 + exp(4)) + (-exp(4) + 3*exp(2))*exp(-x)/(1 + exp(4)))] ``` Here is an example of setting the initial value for the derivative of a function, namely $f'(1) = 2$: ```py >>> eqn = Eq(f(x).diff(x), f(x)) >>> dsolve(eqn, f(x), ics={f(x).diff(x).subs(x, 1): 2}) Eq(f(x), 2*exp(-1)*exp(x)) ``` ### Option 2: Define a Function of an Independent Variable You may prefer to specify a function (for example $y$) of its independent variable (for example $t$), so that `y` represents `y(t)`: ```py >>> from sympy import symbols, Function, dsolve >>> t = symbols('t') >>> y = Function('y')(t) >>> y y(t) >>> yp = y.diff(t) >>> ypp = yp.diff(t) >>> eq = ypp + 2*yp + y >>> eq y(t) + 2*Derivative(y(t), t) + Derivative(y(t), (t, 2)) >>> dsolve(eq, y) Eq(y(t), (C1 + C2*t)*exp(-t)) ``` Using this convention, the second argument of {func}`~.dsolve`, `y`, represents `y(t)`, so SymPy recognizes it as a valid function to solve for. #### Specify Initial Conditions or Boundary Conditions Using that syntax, you specify initialor boundary conditions by substituting in values of the independent variable using {func}`~sympy.core.basic.Basic.subs` because the function $y$ already has its independent variable as an argument $t$: ```py >>> dsolve(eq, y, ics={y.subs(t, 0): 0}) Eq(y(t), C2*t*exp(-t)) ``` #### Beware Copying and Pasting Results If you choose to define a function of an independent variable, note that copying a result and pasting it into subsequent code may cause an error because `x` is already defined as `y(t)`, so if you paste in `y(t)` it is interpreted as `y(t)(t)`: ```py >>> dsolve(y(t).diff(y), y) Traceback (most recent call last): ... TypeError: 'y' object is not callable ``` So remember to exclude the independent variable call `(t)`: ```py >>> dsolve(y.diff(t), y) Eq(y(t), C1) ``` ## Use the Solution Result Unlike other solving functions, {func}`~.dsolve` returns an {class}`~.Equality` (equation) formatted as, for example, `Eq(y(x), C1*sin(3*x) + C2*cos(3*x))` which is equivalent to the mathematical notation $y(x) = C_1 \sin(3x) + C_2 \cos(3x)$. ### Extract the Result for One Solution and Function You can extract the result from an {class}`~.Equality` using the right-hand side property {any}`rhs `: ```py >>> from sympy import Function, dsolve, Derivative >>> from sympy.abc import x >>> y = Function('y') >>> result = dsolve(Derivative(y(x), x, x) + 9*y(x), y(x)) >>> result Eq(y(x), C1*sin(3*x) + C2*cos(3*x)) >>> result.rhs C1*sin(3*x) + C2*cos(3*x) ``` #### Some ODEs Cannot Be Solved Explicitly, Only Implicitly The above ODE can be solved explicitly, specifically $y(x)$ can be expressed in terms of functions of $x$. However, some ODEs cannot be solved explicitly, for example: ```py >>> from sympy import dsolve, exp, symbols, Function >>> f = symbols("f", cls=Function) >>> x = symbols("x") >>> dsolve(f(x).diff(x) + exp(-f(x))*f(x)) Eq(Ei(f(x)), C1 - x) ``` This gives no direct expression for $f(x)$. Instead, {func}`~.dsolve` expresses a solution as $g(f(x))$ where $g$ is {class}`~.Ei`, the classical exponential integral function. `Ei` does not have a known closed-form inverse, so a solution cannot be explicitly expressed as $f(x)$ equaling a function of $x$. Instead, `dsolve` returns an [implicit solution](https://en.wikipedia.org/wiki/Implicit_function). When `dsolve` returns an implicit solution, extracting the right-hand side of the returned equality will not give an explicitly expression for the function to be solved for, here $f(x)$. So before extracting an expression for the function to be solved for, check that `dsolve` was able to solve for the function explicitly. ### Extract the Result for Multiple Function-Solution Pairs If you are solving a system of equations with multiple unknown functions, the form of the output of {func}`~.dsolve` depends on whether there is one or multiple solutions. #### If There is One Solution Set If there is only one solution set to a system of equations with multiple unknown functions, {func}`~.dsolve` will return a non-nested list containing an equality. You can extract the solution expression using a single loop or comprehension: ```py >>> from sympy import symbols, Eq, Function, dsolve >>> y, z = symbols("y z", cls=Function) >>> x = symbols("x") >>> eqs_one_soln_set = [Eq(y(x).diff(x), z(x)**2), Eq(z(x).diff(x), z(x))] >>> solutions_one_soln_set = dsolve(eqs_one_soln_set, [y(x), z(x)]) >>> solutions_one_soln_set [Eq(y(x), C1 + C2**2*exp(2*x)/2), Eq(z(x), C2*exp(x))] >>> # Loop through list approach >>> solution_one_soln_set_dict = {} >>> for fn in solutions_one_soln_set: ... solution_one_soln_set_dict.update({fn.lhs: fn.rhs}) >>> solution_one_soln_set_dict {y(x): C1 + C2**2*exp(2*x)/2, z(x): C2*exp(x)} >>> # List comprehension approach >>> solution_one_soln_set_dict = {fn.lhs:fn.rhs for fn in solutions_one_soln_set} >>> solution_one_soln_set_dict {y(x): C1 + C2**2*exp(2*x)/2, z(x): C2*exp(x)} >>> # Extract expression for y(x) >>> solution_one_soln_set_dict[y(x)] C1 + C2**2*exp(2*x)/2 ``` #### If There are Multiple Solution Sets If there are multiple solution sets to a system of equations with multiple unknown functions, {func}`~.dsolve` will return a nested list of equalities, the outer list representing each solution and the inner list representing each function. While you can extract results by specifying the index of each function, we recommend an approach which is robust with respect to function ordering. The following converts each solution into a dictionary so you can easily extract the result for the desired function. It uses standard Python techniques such as loops or comprehensions, in a nested fashion. ```py >>> from sympy import symbols, Eq, Function, dsolve >>> y, z = symbols("y z", cls=Function) >>> x = symbols("x") >>> eqs = [Eq(y(x).diff(x)**2, z(x)**2), Eq(z(x).diff(x), z(x))] >>> solutions = dsolve(eqs, [y(x), z(x)]) >>> solutions [[Eq(y(x), C1 - C2*exp(x)), Eq(z(x), C2*exp(x))], [Eq(y(x), C1 + C2*exp(x)), Eq(z(x), C2*exp(x))]] >>> # Nested list approach >>> solutions_list = [] >>> for solution in solutions: ... solution_dict = {} ... for fn in solution: ... solution_dict.update({fn.lhs: fn.rhs}) ... solutions_list.append(solution_dict) >>> solutions_list [{y(x): C1 - C2*exp(x), z(x): C2*exp(x)}, {y(x): C1 + C2*exp(x), z(x): C2*exp(x)}] >>> # Nested comprehension approach >>> solutions_list = [{fn.lhs:fn.rhs for fn in solution} for solution in solutions] >>> solutions_list [{y(x): C1 - C2*exp(x), z(x): C2*exp(x)}, {y(x): C1 + C2*exp(x), z(x): C2*exp(x)}] >>> # Extract expression for y(x) >>> solutions_list[0][y(x)] C1 - C2*exp(x) ``` ### Work With Arbitrary Constants You can manipulate arbitrary constants such as `C1`, `C2`, and `C3`, which are generated automatically by {func}`~.dsolve`, by creating them as symbols. For example, if you want to assign values to arbitrary constants, you can create them as symbols and then substitute in their values using {meth}`~sympy.core.basic.Basic.subs`: ```py >>> from sympy import Function, dsolve, Derivative, symbols, pi >>> y = Function('y') >>> x, C1, C2 = symbols("x, C1, C2") >>> result = dsolve(Derivative(y(x), x, x) + 9*y(x), y(x)).rhs >>> result C1*sin(3*x) + C2*cos(3*x) >>> result.subs({C1: 7, C2: pi}) 7*sin(3*x) + pi*cos(3*x) ``` ## Numerically Solve an ODE in SciPy A common workflow which leverages [SciPy's](https://docs.scipy.org/doc/scipy/index.html) fast numerical ODE solving is 1. set up an ODE in SymPy 2. convert it to a numerical function using {func}`~.lambdify` 3. solve the initial value problem by [numerically integrating the ODE using SciPy's `solve_ivp`](https://docs.scipy.org/doc/scipy/reference/integrate.html#solving-initial-value-problems-for-ode-systems). Here is an [example from the field of chemical kinetics](https://www.sympy.org/scipy-2017-codegen-tutorial/notebooks/25-chemical-kinetics-intro.html) where the nonlinear ordinary differential equations take this form: $$ r_f = & k_f y_0(t)^2 y_1(t) \\ r_b = & k_b y_2(t)^2 \\ \frac{d y_0(t)}{dt} = & 2(r_b - r_f) \\ \frac{d y_1(t)}{dt} = & r_b - r_f \\ \frac{d y_2(t)}{dt} = & 2(r_f - r_b) $$ and $$ \vec{y}(t) = \begin{bmatrix} y_0(t) \\ y_1(t) \\ y_2(t) \end{bmatrix} $$ ```{eval-rst} .. plot:: :format: doctest :include-source: True :context: close-figs >>> from sympy import symbols, lambdify >>> import numpy as np >>> import scipy.integrate >>> import matplotlib.pyplot as plt >>> # Create symbols y0, y1, and y2 >>> y = symbols('y:3') >>> kf, kb = symbols('kf kb') >>> rf = kf * y[0]**2 * y[1] >>> rb = kb * y[2]**2 >>> # Derivative of the function y(t); values for the three chemical species >>> # for input values y, kf, and kb >>> ydot = [2*(rb - rf), rb - rf, 2*(rf - rb)] >>> ydot [2*kb*y2**2 - 2*kf*y0**2*y1, kb*y2**2 - kf*y0**2*y1, -2*kb*y2**2 + 2*kf*y0**2*y1] >>> t = symbols('t') # not used in this case >>> # Convert the SymPy symbolic expression for ydot into a form that >>> # SciPy can evaluate numerically, f >>> f = lambdify((t, y, kf, kb), ydot) >>> k_vals = np.array([0.42, 0.17]) # arbitrary in this case >>> y0 = [1, 1, 0] # initial condition (initial values) >>> t_eval = np.linspace(0, 10, 50) # evaluate integral from t = 0-10 for 50 points >>> # Call SciPy's ODE initial value problem solver solve_ivp by passing it >>> # the function f, >>> # the interval of integration, >>> # the initial state, and >>> # the arguments to pass to the function f >>> solution = scipy.integrate.solve_ivp(f, (0, 10), y0, t_eval=t_eval, args=k_vals) >>> # Extract the y (concentration) values from SciPy solution result >>> y = solution.y >>> # Plot the result graphically using matplotlib >>> plt.plot(t_eval, y.T) # doctest: +SKIP >>> # Add title, legend, and axis labels to the plot >>> plt.title('Chemical Kinetics') # doctest: +SKIP >>> plt.legend(['NO', 'Br$_2$', 'NOBr'], shadow=True) # doctest: +SKIP >>> plt.xlabel('time') # doctest: +SKIP >>> plt.ylabel('concentration') # doctest: +SKIP >>> # Finally, display the annotated plot >>> plt.show() ``` SciPy's `solve_ivp` returns a result containing `y` (numerical function result, here, concentration) values for each of the three chemical species, corresponding to the time points `t_eval`. ## Ordinary Differential Equation Solving Hints ### Return Unevaluated Integrals By default, {func}`~.dsolve` attempts to evaluate the integrals it produces to solve your ordinary differential equation. You can disable evaluation of the integrals by using {ref}`hints` ending with `_Integral`, for example `separable_Integral`. This is useful because {func}`~sympy.core.expr.Expr.integrate` is an expensive routine. SymPy may hang (appear to never complete the operation) because of a difficult or impossible integral, so using an `_Integral` hint will at least return an (unintegrated) result, which you can then consider. The simplest way to disable integration is with the `all_Integral` hint because you do not need to know which hint to supply: for any hint with a corresponding `_Integral` hint, `all_Integral` only returns the `_Integral` hint. ### Select a Specific Solver You may wish to select a specific solver using a hint for a couple of reasons: - educational purposes: for example if you are learning about a specific method to solve ODEs and want to get a result that exactly matches that method - form of the result: sometimes an ODE can be solved by many different solvers, and they can return different results. They will be mathematically equivalent, though the arbitrary constants may not be. {func}`~.dsolve` by default tries to use the "best" solvers first, which are most likely to return the most usable output, but it is not a perfect heuristic. For example, the "best" solver may produce a result with an integral that SymPy cannot solve, but another solver may produce a different integral that SymPy can solve. So if the solution isn't in a form you like, you can try other hints to check whether they give a preferable result. ## Not All Equations Can Be Solved ### Equations With No Solution Not all differential equations can be solved, for example: ```py >>> from sympy import Function, dsolve, Derivative, symbols >>> y = Function('y') >>> x, C1, C2 = symbols("x, C1, C2") >>> dsolve(Derivative(y(x), x, 3) - (y(x)**2), y(x)).rhs Traceback (most recent call last): ... NotImplementedError: solve: Cannot solve -y(x)**2 + Derivative(y(x), (x, 3)) ``` ### Equations With No Closed-Form Solution As noted above, [](#some-odes-cannot-be-solved-explicitly-only-implicitly). Also, some systems of differential equations have no closed-form solution because they are chaotic, for example the [Lorenz system](https://en.wikipedia.org/wiki/Lorenz_system#Overview) or a double pendulum described by these two differential equations (simplified from [ScienceWorld](https://scienceworld.wolfram.com/physics/DoublePendulum.html)): $$ 2 \theta_1''(t) + \theta_2''(t) \cos(\theta_1-\theta_2) + \theta_2'^2(t) \sin(\theta_1 - \theta_2) + 2g \sin(\theta_1) = 0 $$ $$ \theta_2''(t) + \theta_1''(t) \cos(\theta_1-\theta_2) - \theta_1'^2(t) \sin(\theta_1 - \theta_2) + g \sin(\theta_2) = 0 $$ ```py >>> from sympy import symbols, Function, cos, sin, dsolve >>> theta1, theta2 = symbols('theta1 theta2', cls=Function) >>> g, t = symbols('g t') >>> eq1 = 2*theta1(t).diff(t, t) + theta2(t).diff(t, t)*cos(theta1(t) - theta2(t)) + theta2(t).diff(t)**2*sin(theta1(t) - theta2(t)) + 2*g*sin(theta1(t)) >>> eq2 = theta2(t).diff(t, t) + theta1(t).diff(t, t)*cos(theta1(t) - theta2(t)) - theta1(t).diff(t)**2*sin(theta1(t) - theta2(t)) + g*sin(theta2(t)) >>> dsolve([eq1, eq2], [theta1(t), theta2(t)]) Traceback (most recent call last): ... NotImplementedError ``` For such cases, you can solve the equations numerically as mentioned in [](#alternatives-to-consider). ## Report a Bug If you find a bug with {func}`~.dsolve`, please post the problem on the [SymPy mailing list](https://groups.google.com/g/sympy). Until the issue is resolved, you can use a different method listed in [](#alternatives-to-consider).