6.1. Simulation and Optimal Control in Pharmacokinetics#

6.1.1. Installations and Initialization#

Pyomo and ipopt are now preinstalled on Google Colaboratory. On MacOS and Windows PC, a one-time installation of pyomo and ipopt is required. The following commands will perform the needed installation when using the Anaconda distribution of Python.

!wget -N -q "https://ampl.com/dl/open/ipopt/ipopt-linux64.zip"
!unzip -o -q ipopt-linux64
ipopt_executable = '/content/ipopt'
import matplotlib.pyplot as plt
import pyomo.environ as pyo
import pyomo.dae as dae

On MacOS and Windows PC, a one-time installation of pyomo and ipopt is required. The following commands will perform the needed installation when using the Anaconda distribution of Python.

!conda install -c conda-forge pyomo
!conda install -c conda-forge pyomo.extras
!conda install -c conda-forge ipopt

6.1.2. A Pharmacokinetics Example#

Pharmacokinetics – the study of the drugs and other substances administered to a living organism – is a rich source of examples for simulation and optimization. Here we discuss a solution to a very simple problem.

A patient is admitted to a clinic with a dangerous blood borne infection that needs to be treated with a specific antibiotic. The treatment requires a minimum therapeutic concentration of 20 mg/liter of blood, but, for safety reasons, should not exceed 40 mg/liter. The attending physician has established a target value of 25 mg/liter. The antibiotic will be administered to the patient by continuous infusion over a 72 hour period using a pump capable of delivering up to 50 mg/hour.

Infusionspumpe

(photo from Pflegewiki-User Würfel, CC BY-SA 3.0)

The research literature shows dynamics of antibiotic concentration in the body behaves according to a one-compartment pharmacokinetic model

\[\begin{align*} V\frac{dC}{dt} & = u(t) - k C \end{align*}\]

where \(V\) is compartment volume (e.g., blood is about 5 liters in an adult), and \(kC\) is the rate of elimination due to all sources, including metabolism and excretion. Based on an observed half-life in the blood of 8 hours, \(k\) has an estimated value of 0.625 liters/hour. Given this information, what administration policy would you recommend?

6.1.3. Simulation of Fixed Infusion Rate Strategy#

The first strategy we will investigate is constant infusion. At steady-state, the pharmacokinetic model becomes

\[\begin{align*} \bar{u} & = k \bar{C} \\ & = 0.625 \times 25 \\ & = 15.625 \mbox{ mg/hr} \end{align*}\]

The following cell simulates the patient response.

# parameters
t_final = 72     # hours
V = 5.0          # liters
k = 0.625        # liters/hour
u = 25*0.625     # mg/hour infusion rate

# create a model
model = pyo.ConcreteModel()

# define time as a continous set with bounds
model.t = dae.ContinuousSet(bounds=(0, t_final))

# define problem variable indexed by time
model.C = pyo.Var(model.t, domain=pyo.NonNegativeReals)
model.dCdt = dae.DerivativeVar(model.C, wrt=model.t)

# the differential equation is a constraint indexed by time
@model.Constraint(model.t)
def ode(model, t):
    return V*model.dCdt[t] == u - k*model.C[t]

# initial condition
model.C[0].fix(0)

# transform the model to a system of algebraic constraints
TransformationFactory('dae.finite_difference').apply_to(model, nfe=50, method='backwards')

# compute a solution using the Pyomo simulator capability
tsim, profiles = Simulator(model, package='ioppt').simulate()

# plot results
plt.plot(tsim, profiles)
plt.xlabel('time / hours')
plt.ylabel('mg/liter')
plt.title('Blood conctration for a steady infusion of ' + str(u) + ' mg/hr.');
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [14], in <cell line: 26>()
     23 model.C[0].fix(0)
     25 # transform the model to a system of algebraic constraints
---> 26 TransformationFactory('dae.finite_difference').apply_to(model, nfe=50, method='backwards')
     28 # compute a solution using the Pyomo simulator capability
     29 tsim, profiles = Simulator(model, package='ioppt').simulate()

File ~/opt/anaconda3/lib/python3.9/site-packages/pyomo/core/base/transformation.py:69, in Transformation.apply_to(self, model, **kwds)
     67 if not hasattr(model, '_transformation_data'):
     68     model._transformation_data = TransformationData()
---> 69 self._apply_to(model, **kwds)
     70 timer.report()

File ~/opt/anaconda3/lib/python3.9/site-packages/pyomo/dae/plugins/finitedifference.py:169, in Finite_Difference_Transformation._apply_to(self, instance, **kwds)
    153 def _apply_to(self, instance, **kwds):
    154     """
    155     Applies the transformation to a modeling instance
    156 
   (...)
    166                   scheme is the backward difference method
    167     """
--> 169     config = self.CONFIG(kwds)
    171     tmpnfe = config.nfe
    172     tmpds = config.wrt

File ~/opt/anaconda3/lib/python3.9/site-packages/pyomo/common/config.py:1231, in ConfigBase.__call__(self, value, default, domain, description, doc, visibility, implicit, implicit_domain, preserve_implicit)
   1229 # ... and set the value, if appropriate
   1230 if value is not NoArgumentGiven:
-> 1231     ans.set_value(value)
   1232 return ans

File ~/opt/anaconda3/lib/python3.9/site-packages/pyomo/common/config.py:2177, in ConfigDict.set_value(self, value, skip_implicit)
   2175             _implicit.append(key)
   2176         else:
-> 2177             raise ValueError(
   2178                 "key '%s' not defined for ConfigDict '%s' and "
   2179                 "implicit (undefined) keys are not allowed" %
   2180                 (key, self.name(True)))
   2182 # If the set_value fails part-way through the new values, we
   2183 # want to restore a deterministic state.  That is, either
   2184 # set_value succeeds completely, or else nothing happens.
   2185 _old_data = self.value(False)

ValueError: key 'method' not defined for ConfigDict '' and implicit (undefined) keys are not allowed

6.1.4. Optimizing the Infusion Strategy#

The simulation results show that a constant rate of infusion results in a prolonged period, more than twelve hours, where the antibiotic concentration is below the minimum therapuetic dose of 20 mg/liter. Can we find a better strategy? In particular, can a time-varying strategy yield reduce the time needed to achieve the therapuetic level?

For this case, we consider \(u(t)\) to be a time-varying infusion rate bounded by the capabilities of the infusion pump.

\[\begin{align*} 0 \leq u(t) \leq 50 \mbox{ mg/hr} \end{align*}\]

Next we need to express the objective of maintaining the antibiotic concentration at or near a value \(C_{SP}\) = 25 mg/liter. For this purpose, we define the objective as minimizing the integral square error defined as

\[\begin{align*} \mbox{ISE} & = \int_0^{t_f} (C(t) - C_{SP})^2 \, dt \end{align*}\]

The next cell demonstrates how to formulate the model in Pyomo, and compute a solution using ipopt.

# parameters
t_final = 72     # hours
V = 5.0          # liters
k = 0.625        # liters/hour
u_max = 50       # mg/hour infusion rate
Csp = 25         # setpoint

# create a model
model = ConcreteModel()

# define time as a continous set with bounds
model.t = ContinuousSet(bounds=(0, t_final))

# define problem variable indexed by time
model.u = Var(model.t, bounds=(0, u_max))
model.C = Var(model.t, domain=NonNegativeReals)
model.dCdt = DerivativeVar(model.C, wrt=model.t)

# the differential equation is a constraint indexed by time
model.ode = Constraint(model.t, rule=lambda model, t: V*model.dCdt[t] == model.u[t] - k*model.C[t])

# initial condition
model.C[0].fix(0)

# objective
model.ise = Integral(model.t, wrt=model.t, rule=lambda model, t: (model.C[t] - Csp)**2)
model.obj = Objective(expr=model.ise, sense=minimize)

# transform the model to a system of algebraic constraints
TransformationFactory('dae.finite_difference').apply_to(model, nfe=200, method='backwards')

# simulation
SolverFactory('ipopt', executable=ipopt_executable).solve(model)

# extract solutions from the model
tsim = [t for t in model.t]
Csim = [model.C[t]() for t in model.t]
usim = [model.u[t]() for t in model.t]

# plot results
plt.figure(figsize=(8,6))
plt.subplot(2,1,1)
plt.plot(tsim, Csim)
plt.title('Blood concentration of antibiotic')
plt.xlabel('time / hours')
plt.ylabel('mg/liter')

plt.subplot(2,1,2)
plt.plot(tsim, usim)
plt.ylim((0, 1.1*u_max))
plt.title('Infusion rate')
plt.xlabel('time / hours')
plt.ylabel('mg/hour')

plt.tight_layout()
../_images/bfcdda29fbdc78546e1a605ccb228e0316768b4ab3a41859606de4c99b9a984c.png

The result of this optimization is a much better therapy for the patient. Therapuetic levels of antibiotic are reach with 3 hours thereby leading to an earlier outcome and the possibility of an earlier recovery.

6.1.5. Alternative Objectives#

# parameters
t_final = 72     # hours
V = 5.0          # liters
k = 0.625        # liters/hour
u_max = 50       # mg/hour infusion rate
C_mtd = 20         # setpoint

# create a model
model = ConcreteModel()

# define time as a continous set with bounds
model.t = ContinuousSet(bounds=(0, t_final))

# define problem variable indexed by time
model.err = Var(model.t, domain=NonNegativeReals)
model.u = Var(model.t, bounds=(0, u_max))
model.C = Var(model.t, bounds=(0, 25))
model.dCdt = DerivativeVar(model.C, wrt=model.t)

# the differential equation is a constraint indexed by time
model.ode = Constraint(model.t, rule=lambda model, t: V*model.dCdt[t] == model.u[t] - k*model.C[t])

# initial condition
model.C[0].fix(0)

# objective
model.edef = Constraint(model.t, rule=lambda model, t: model.err[t] >= C_mtd - model.C[t])
model.iae = Integral(model.t, wrt=model.t, rule=lambda model, t: model.err[t]**2)
model.obj = Objective(expr=model.iae, sense=minimize)

# transform the model to a system of algebraic constraints
TransformationFactory('dae.finite_difference').apply_to(model, nfe=200, method='backwards')

# simulation
SolverFactory('ipopt', executable=ipopt_executable).solve(model)

# extract solutions from the model
tsim = [t for t in model.t]
Csim = [model.C[t]() for t in model.t]
usim = [model.u[t]() for t in model.t]

# plot results
plt.figure(figsize=(8,6))
plt.subplot(2,1,1)
plt.plot(tsim, Csim)
plt.title('Blood concentration of antibiotic')
plt.xlabel('time / hours')
plt.ylabel('mg/liter')

plt.subplot(2,1,2)
plt.plot(tsim, usim)
plt.ylim((0, 1.1*u_max))
plt.title('Infusion rate')
plt.xlabel('time / hours')
plt.ylabel('mg/hour')

plt.tight_layout()
../_images/5106ba7b8a773e00078d850af81ec8a9b6b120a81ac0f7b2139d60a711db74c8.png

< Simulation and Optimal Control | Contents | Soft Landing a Rocket >

Open in Colab

Download