In [2]:
    
# 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 [15]:
    
init = State(y = 2)
system = System(init=init, t_0=1, t_end=3)
    
    Out[15]:
In [16]:
    
def slope_func(state, t, system):
    [y] = state
    dydt = y + t
    return [dydt]
    
In [17]:
    
results, details = run_euler(system, slope_func)
    
In [18]:
    
get_last_value(results.y)
    
    Out[18]:
In [2]:
    
data = pd.read_csv('data/glucose_insulin.csv', index_col='time');
    
Interpolate the insulin data.
In [3]:
    
I = interpolate(data.insulin)
    
    Out[3]:
Initialize the parameters
In [4]:
    
G0 = 290
k1 = 0.03
k2 = 0.02
k3 = 1e-05
    
    Out[4]:
To estimate basal levels, we'll use the concentrations at t=0.
In [5]:
    
Gb = data.glucose[0]
Ib = data.insulin[0]
    
    Out[5]:
Create the initial condtions.
In [6]:
    
init = State(G=G0, X=0)
    
    Out[6]:
Make the System object.
In [7]:
    
t_0 = get_first_label(data)
t_end = get_last_label(data)
    
    Out[7]:
In [8]:
    
system = System(G0=G0, k1=k1, k2=k2, k3=k3,
                init=init, Gb=Gb, Ib=Ib, I=I,
                t_0=t_0, t_end=t_end, dt=2)
    
    Out[8]:
In [9]:
    
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)
    
In [10]:
    
def run_simulation(system, update_func):
    """Runs a simulation of the system.
        
    system: System object
    update_func: function that updates state
    
    returns: TimeFrame
    """
    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
    
In [11]:
    
%time results = run_simulation(system, update_func);
    
    
In [12]:
    
results
    
    Out[12]:
In the previous chapter, we approximated the differential equations with difference equations, and solved them using run_simulation.
In this chapter, we solve the differential equation numerically using run_euler...
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(init, 0, system)
    
    Out[14]:
Here's how we run the ODE solver.
In [15]:
    
system = System(G0=G0, k1=k1, k2=k2, k3=k3,
                init=init, Gb=Gb, Ib=Ib, I=I,
                t_0=t_0, t_end=t_end, dt=1)
%time results2, details = run_euler(system, slope_func)
    
    
results is a TimeFrame with one row for each time step and one column for each state variable:
In [16]:
    
results2
    
    Out[16]:
Plotting the results from run_simulation and run_euler, we can see that they are not very different.
In [17]:
    
plot(results.G, '-')
plot(results2.G, '-')
plot(data.glucose, 'bo')
    
    Out[17]:
    
The differences in G are less than 1%.
In [18]:
    
diff = results.G - results2.G
percent_diff = diff / results2.G * 100
max(abs(percent_diff.dropna()))
    
    Out[18]:
In [ ]:
    
    
In [ ]:
    
    
In [19]:
    
m = UNITS.meter
s = UNITS.second
    
    Out[19]:
And defining the initial state.
In [20]:
    
init = State(y=381 * m, 
             v=0 * m/s)
    
    Out[20]:
Acceleration due to gravity is about 9.8 m / s$^2$.
In [21]:
    
g = 9.8 * m/s**2
    
    Out[21]:
When we call odeint, we need an array of timestamps where we want to compute the solution.
I'll start with a duration of 10 seconds.
In [22]:
    
t_end = 10 * s
    
    Out[22]:
Now we make a System object.
In [23]:
    
system = System(init=init, g=g, t_end=t_end)
    
    Out[23]:
And define the slope function.
In [24]:
    
def slope_func(state, t, system):
    """Compute derivatives of the state.
    
    state: position, velocity
    t: time
    system: System object containing `g`
    
    returns: derivatives of y and v
    """
    y, v = state
    g = system.g    
    dydt = v
    dvdt = -g
    
    return dydt, dvdt
    
It's always a good idea to test the slope function with the initial conditions.
In [25]:
    
dydt, dvdt = slope_func(system.init, 0, system)
print(dydt)
print(dvdt)
    
    
Now we're ready to call run_euler
In [26]:
    
system.set(dt=0.1*s)
results, details = run_euler(system, slope_func, max_step=0.5)
details.message
    
    Out[26]:
In [27]:
    
results
    
    Out[27]:
In [28]:
    
def crossings(series, value):
    """Find the labels where the series passes through value.
    The labels in series must be increasing numerical values.
    series: Series
    value: number
    returns: sequence of labels
    """
    units = get_units(series.values[0])
    values = magnitudes(series - value)
    interp = InterpolatedUnivariateSpline(series.index, values)
    return interp.roots()
    
In [29]:
    
t_crossings = crossings(results.y, 0)
    
    Out[29]:
In [30]:
    
system.set(dt=0.1*s)
results, details = run_ralston(system, slope_func, max_step=0.5)
details.message
    
    Out[30]:
In [31]:
    
t_crossings = crossings(results.y, 0)
    
    Out[31]:
Here are the results:
In [32]:
    
results
    
    Out[32]:
And here's position as a function of time:
In [33]:
    
def plot_position(results):
    plot(results.y, label='y')
    decorate(xlabel='Time (s)',
             ylabel='Position (m)')
plot_position(results)
savefig('figs/chap09-fig01.pdf')
    
    
    
In [34]:
    
def crossings(series, value):
    """Find the labels where the series passes through value.
    The labels in series must be increasing numerical values.
    series: Series
    value: number
    returns: sequence of labels
    """
    units = get_units(series.values[0])
    values = magnitudes(series - value)
    interp = InterpolatedUnivariateSpline(series.index, values)
    return interp.roots()
    
In [35]:
    
t_crossings = crossings(results.y, 0)
    
    Out[35]:
For this example there should be just one crossing, the time when the penny hits the sidewalk.
In [36]:
    
t_sidewalk = t_crossings[0] * s
    
    Out[36]:
We can compare that to the exact result. Without air resistance, we have
$v = -g t$
and
$y = 381 - g t^2 / 2$
Setting $y=0$ and solving for $t$ yields
$t = \sqrt{\frac{2 y_{init}}{g}}$
In [37]:
    
sqrt(2 * init.y / g)
    
    Out[37]:
The estimate is accurate to about 10 decimal places.
Instead of running the simulation until the penny goes through the sidewalk, it would be better to detect the point where the penny hits the sidewalk and stop.  run_ralston provides exactly the tool we need, event functions.
Here's an event function that returns the height of the penny above the sidewalk:
In [38]:
    
def event_func(state, t, system):
    """Return the height of the penny above the sidewalk.
    """
    y, v = state
    return y
    
And here's how we pass it to run_ralston.  The solver should run until the event function returns 0, and then terminate.
In [39]:
    
results, details = run_ralston(system, slope_func, events=event_func)
details
    
    Out[39]:
The message from the solver indicates the solver stopped because the event we wanted to detect happened.
Here are the results:
In [40]:
    
results
    
    Out[40]:
With the events option, the solver returns the actual time steps it computed, which are not necessarily equally spaced.
The last time step is when the event occurred:
In [41]:
    
t_sidewalk = get_last_label(results) * s
    
    Out[41]:
The result is accurate to about 15 decimal places.
We can also check the velocity of the penny when it hits the sidewalk:
In [42]:
    
v_sidewalk = get_last_value(results.v)
    
    Out[42]:
And convert to kilometers per hour.
In [43]:
    
km = UNITS.kilometer
h = UNITS.hour
v_sidewalk.to(km / h)
    
    Out[43]:
If there were no air resistance, the penny would hit the sidewalk (or someone's head) at more than 300 km/h.
So it's a good thing there is air resistance.