
Ordinary Differential Equation (ODE) can be used to describe a dynamic system. To some extent, we are living in a dynamic system, the weather outside of the window changes from dawn to dusk, the metabolism occurs in our body is also a dynamic system because thousands of reactions and molecules got synthesized and degraded as time goes.
More formally, if we define a set of variables, like the temperatures in a day, or the amount of molecule X in a certain time point, and it changes with the independent variable (in a dynamic system, usually it will be time t). ODE offers us a way to mathematically depict the dynamic changes of defined variables. The opposite system to that is called static system, thinking about taking a photo of the outside, this snapshot doesn’t contain any dynamics, in another word, it is static.
Solving Ordinary Differential Equations means determining how the variables will change as time goes by, the solution, sometimes referred to as solution curve (visually shown as below), provide informative prediction to the default behavior of any dynamic systems.

In this article, I am going to give an introduction to ODE and more important, how to solve ODE merely using Python.
A sweep of terminology
Here I firstly introduce some terminologies from which readers may benefit. Ordinary Differential Equation (ODE) looks something like this:

It is an equation that involves the derivatives, but no partial derivative in the equation itself. In another word, we only consider one independent variable, that is time t. When we have more than one independent variable coming in, it becomes Partial Differential Equation (PDE), which is not within the scope of this article.

In ODE, if the coefficient of terms that contains dependent variable (in the above case, the variable R, the term contains R includes dR/dt and k2R) are independent of R, this ODE is regarded as linear ODE. To illustrate the point, let me show you a non-linear ODE as a comparison:

As you can see, now the coefficient of dR/dt is R, which violates the definition of linear ODE, so it turns into a non-linear ODE.
Finally, the last pair of terminology, if the derivative of variable is independent of the independent variable (here it is time t), we call them autonomous ODE, otherwise, it becomes a non-autonomous ODE. Again, a non-autonomous ODE is shown below:

You can see now time t is on the right-hand side, so it is a non-autonomous ODE.
Solving a sigmoidal signal-response curve
First and foremost, the examples used in this article come from a marvelous review paper from Tyson et al. I would highly recommend checking it out if you are interested in that.
A sigmoidal signal-response curve describes a system like below:

It is the phosphorylation process that happened in our body, R is the substance, which will be converted to the phosphorylated form Rp to exert a lot of functions, this process is catalyzed by Signal S, and the ATP hydrolysis accompanying that. To describe this process, the original paper utilized the following ODE:

Now we first want to solve this equation, meaning to obtain the solution curve that describes how Rp would change with time.
Let’s start to code:
from scipy.integrate import odeint
import numpy as np
import matplotlib.pyplot as plt
def model(Rp,t,S):
k1 = 1
k2 = 1
Rt = 1
km1 = 0.05
km2 = 0.05
dRpdt = (k1*S*(Rt-Rp)/(km1+Rt-Rp)) - k2*Rp/(km2+Rp)
return dRpdt
The above code is just to set up the model as we described using a mathematical equation, here some parameters I just used what has been provided in the original paper.
We set the signal strength as 1, but it is really just for illustration, feel free to change it to whatever value you prefer. Then we set the initial value of Rp to three different possibilities: 0, 0.3, and 1. It can show us how different initialization will finally converge to the steady-state. We set the simulation time window from 0 to 20 just for simplicity. Finally, we use odeint
function to solve this ODE.
S = 1
Rp0 = [0,0.3,1]
t = np.linspace(0,20,200)
result = odeint(model,Rp0,t,args=(S,))
The result
object is a NumPy array of the shape [200,3], 3 corresponds to three initialization conditions. Now we plot that:
fig,ax = plt.subplots()
ax.plot(t,result[:,0],label='R0=0')
ax.plot(t,result[:,1],label='R0=0.3')
ax.plot(t,result[:,2],label='R0=1')
ax.legend()
ax.set_xlabel('t')
ax.set_ylabel('Rp')

Here we can see, no matter where the Rp starts, they will converge to somewhere around 0.5, and this is called a steady-state. Steady-state is the gist of ODE because it defines the default behavior of a dynamic system. You may wonder why we call it a sigmoidal signal response curve? That is because apart from solving the solution curve, we are also interested in knowing how the signal strength will act upon the whole system, in ODE language, how signal strength will change the steady-state of the system? Now we are going to explore it!
Mathematically, a steady-state of a system is basically the root of the formula when setting dRp/dt = 0. A better illustration is as below:

So if we can solve the last equation with respect to Rp, we will have the value of Rp in the steady-state.
S_all = np.linspace(0,3,100)
def equation(Rp,S):
k1 = 1
k2 = 1
Rt = 1
km1 = 0.05
km2 = 0.05
return k1*S*(Rt-Rp)/(km1+Rt-Rp) - k2*Rp/(km2+Rp)
from scipy.optimize import fsolve
store = []
for S in S_all:
Rp_ss = fsolve(equation,[1],args=(S,))[0]
store.append(Rp_ss)
We first set the range of signal S from 0–3, then we use fsolve
function from scipy.optimize
to do the job. The result basically will be the Rp value when S is equal to the different values within 0–3.
fig,ax = plt.subplots()
ax.plot(S_all,store,c='k')
ax.set_xlim(0,3)
ax.set_xlabel('Signal(S)')
ax.set_ylim(0,1.1)
ax.set_ylabel('Response(R_ss)')
Now let’s look at the result:

Now I hope it becomes clear why it is called the sigmoidal signal-response curve because as the strength of the signal changes, the steady-state of the system will respond in a sigmoidal manner.
Further remark
The above example is just to let you get a taste of what ODE is and how to use python to solve ODE in just a few lines of code. When the system becomes more complicated, for example, more than 1 components get involved (here we referred to as the first-order ODE), another python package called GEKKO or scipy.integrate.solve_ivp may help you do the job.
If we are interested in how to reproduce other figures in Tyson et al. I would recommend you check my Github repository where all the codes are provided.
https://github.com/frankligy/ScPyT/blob/main/ODE/submission/Frank_system_biology_hw.pdf
I hope you find this article interesting and useful, thanks for reading! If you like this article, follow me on medium, thank you so much for your support. Connect me on my Twitter or LinkedIn, also please let me know if you have any questions or what kind of tutorials you would like to see In the future!