In [1]:
    
# Configure Jupyter so figures appear in the notebook
%matplotlib inline
# Configure Jupyter to display the assigned value after an assignment
%config InteractiveShell.ast_node_interactivity='last_expr_or_assign'
# import functions from the modsim.py module
from modsim import *
    
In [2]:
    
data = pd.read_csv('data/glucose_insulin.csv', index_col='time');
    
Interpolate the insulin data.
In [3]:
    
I = interpolate(data.insulin)
    
In [4]:
    
params = Params(G0 = 290,
                k1 = 0.03,
                k2 = 0.02,
                k3 = 1e-05)
    
Here's a version of make_system that takes the parameters and data:
In [5]:
    
def make_system(params, data):
    """Makes a System object with the given parameters.
    
    params: sequence of G0, k1, k2, k3
    data: DataFrame with `glucose` and `insulin`
    
    returns: System object
    """
    G0, k1, k2, k3 = params
    
    Gb = data.glucose[0]
    Ib = data.insulin[0]
    I = interpolate(data.insulin)
    
    t_0 = get_first_label(data)
    t_end = get_last_label(data)
    init = State(G=G0, X=0)
    
    return System(params,
                  init=init, Gb=Gb, Ib=Ib, I=I,
                  t_0=t_0, t_end=t_end, dt=2)
    
In [6]:
    
system = make_system(params, data)
    
And here's the update function.
In [7]:
    
def update_func(state, t, system):
    """Updates the glucose minimal model.
    
    state: State object
    t: time in min
    system: System object
    
    returns: State object
    """
    G, X = state
    k1, k2, k3 = system.k1, system.k2, system.k3 
    I, Ib, Gb = system.I, system.Ib, system.Gb
    dt = system.dt
        
    dGdt = -k1 * (G - Gb) - X*G
    dXdt = k3 * (I(t) - Ib) - k2 * X
    
    G += dGdt * dt
    X += dXdt * dt
    return State(G=G, X=X)
    
Before running the simulation, it is always a good idea to test the update function using the initial conditions. In this case we can veryify that the results are at least qualitatively correct.
In [8]:
    
update_func(system.init, system.t_0, system)
    
Now run_simulation is pretty much the same as it always is.
In [9]:
    
def run_simulation(system, update_func):
    """Runs a simulation of the system.
        
    system: System object
    update_func: function that updates state
    
    returns: TimeFrame
    """
    init = system.init
    t_0, t_end, dt = system.t_0, system.t_end, system.dt
    
    frame = TimeFrame(columns=init.index)
    frame.row[t_0] = init
    ts = linrange(t_0, t_end, dt)
    
    for t in ts:
        frame.row[t+dt] = update_func(frame.row[t], t, system)
    
    return frame
    
And here's how we run it.
In [10]:
    
results = run_simulation(system, update_func);
    
The results are in a TimeFrame object with one column per state variable.
In [11]:
    
results
    
The following plot shows the results of the simulation along with the actual glucose data.
In [12]:
    
subplot(2, 1, 1)
plot(results.G, 'b-', label='simulation')
plot(data.glucose, 'bo', label='glucose data')
decorate(ylabel='Concentration (mg/dL)')
subplot(2, 1, 2)
plot(results.X, 'C1', label='remote insulin')
decorate(xlabel='Time (min)', 
         ylabel='Concentration (arbitrary units)')
savefig('figs/chap18-fig01.pdf')
    
Now let's solve the differential equation numerically using run_ode_solver, which is an implementation of Ralston's method.
Instead of an update function, we provide a slope function that evaluates the right-hand side of the differential equations.
We don't have to do the update part; the solver does it for us.
In [13]:
    
def slope_func(state, t, system):
    """Computes derivatives of the glucose minimal model.
    
    state: State object
    t: time in min
    system: System object
    
    returns: derivatives of G and X
    """
    G, X = state
    k1, k2, k3 = system.k1, system.k2, system.k3 
    I, Ib, Gb = system.I, system.Ib, system.Gb
    
    dGdt = -k1 * (G - Gb) - X*G
    dXdt = k3 * (I(t) - Ib) - k2 * X
    
    return dGdt, dXdt
    
We can test the slope function with the initial conditions.
In [14]:
    
slope_func(system.init, 0, system)
    
Here's how we run the ODE solver.
In [15]:
    
results2, details = run_ode_solver(system, slope_func)
    
details is a ModSimSeries object with information about how the solver worked.
In [16]:
    
details
    
results is a TimeFrame with one row for each time step and one column for each state variable:
In [17]:
    
results2
    
Plotting the results from run_simulation and run_ode_solver, we can see that they are not very different.
In [18]:
    
plot(results.G, 'C0', label='run_simulation')
plot(results2.G, 'C2--', label='run_ode_solver')
decorate(xlabel='Time (min)', ylabel='Concentration (mg/dL)')
savefig('figs/chap18-fig02.pdf')
    
The differences in G are less than 2%.
In [19]:
    
diff = results.G - results2.G
percent_diff = diff / results2.G * 100
percent_diff
    
In [20]:
    
max(abs(percent_diff))
    
Exercise:  Our solution to the differential equations is only approximate because we used a finite step size, dt=2 minutes.
If we make the step size smaller, we expect the solution to be more accurate.  Run the simulation with dt=1 and compare the results.  What is the largest relative error between the two solutions?
In [21]:
    
# Solution goes here
    
In [22]:
    
# Solution goes here
    
In [23]:
    
# Solution goes here
    
In [24]:
    
# Solution goes here
    
Here's the source code for run_ode_solver if you'd like to know how it works.
Notice that run_ode_solver is another name for run_ralston, which implements Ralston's method.
In [25]:
    
source_code(run_ode_solver)
    
Related reading: You might be interested in this article about people making a DIY artificial pancreas.
In [ ]: