rleast-squaresdata-fittingpdemodel-fitting

Fitting cell growth model (Monod) with 3 PDEs in R


Updated The problem is solved with the answer from @tpetzoldt, the original code has been modified to run the fit successfully.

Hi everybody, I'm trying to fit the experimental data on a set of 3 PDEs to find 4 coefficients including mumax, Ks, Y_(x/s), and Y_(p/s). The code I used worked with the set of 2 PDEs but is not working with this set of 3. The following is the code:

The set of PDEs needs to be fitted:

dX/dt = mumax . S . X / (Ks + S)

dS/dt = -1/Y_(x/s) . mumax . S . X / (Ks + S)

dP/dt = alpha . dX/dt + beta . X

library(deSolve)
library(ggplot2) 
library(minpack.lm) 
library(reshape2)

time <- c(0, 3, 5, 8, 9.5, 11.5, 14, 16, 18, 20, 25, 27)
X <- c(0.0904, 0.1503, 0.2407, 0.3864, 0.5201, 0.6667, 0.8159, 0.9979, 1.0673, 1.1224, 1.1512, 1.2093)
S <- c(9.0115, 8.8088, 7.9229, 7.2668, 5.3347, 4.911, 3.5354, 1.4041, 0, 0, 0, 0)
P <- c(0.0151, 0.0328, 0.0621, 0.1259, 0.2949, 0.3715, 0.4199, 0.522, 0.5345, 0.6081, 0.07662, 0.7869)
data <- data.frame(time, X, S, P)

Monod <- function(t,c,parms) {
  k1 <- parms$k1 # mumax
  k2 <- parms$k2 # Ks
  k3 <- parms$k3 # Y_(X/S)
  k4 <- parms$k4 # alpha
  k5 <- parms$k5 # beta
  r <- numeric(length(c))
  r[1] <-  k1 * c["S"] * c["X"] / ( k2 + c["S"] ) # r[1] = dCx/dt = k1.Cs.Cx/(k2+Cs)
  r[2] <- -k1 * c["S"] * c["X"] / ( ( k2 + c["S"] ) * k3 ) # r[2] = dCs/dt = -1/k3 * dCx/dt 
  r[3] <-  k4 * r[1] + k5 * c["X"] # r[3] = dCp/dt = alpha * dX/dt + beta * X
  return(list(r))       
}

residuals <- function(parms){
  cinit <- c( X=data[1,2], S=data[1,3], P=data[1,4] )
  
  t <- c(seq(0, 27, 1), data$time)
  t <- sort(unique(t))
  
  k1 <- parms[1]
  k2 <- parms[2]
  k3 <- parms[3]
  k4 <- parms[4]
  k5 <- parms[5]
  
  out <- ode( y = cinit, 
              times = t, 
              func = Monod, 
              parms = list( k1 = k1, k2 = k2, k3 = k3, k4 = k4, k5 = k5) )
  
  out_Monod <- data.frame(out)
  out_Monod <- out_Monod[out_Monod$t %in% data$time,]
  
  pred_Monod <- melt(out_Monod,id.var="time",variable.name="Substance",value.name="Conc")
  exp_Monod <- melt(data,id.var="time",variable.name="Substance",value.name="Conc")
  residuals <- pred_Monod$Conc-exp_Monod$Conc
  
  return(residuals)
}

parms <- c(k1=0.5, k2=6.5, k3=0.2, k4=1.2, k5 = 0.1)
fitval <- nls.lm(par=parms,fn=residuals)
cinit <- c(X=data[1,2], S=data[1,3], P=data[1,4])
out <- ode(y=cinit, times=seq(min(data$time), max(data$time)), func = Monod, parms=as.list(coef(fitval)))
plot(out, obs=data, mfrow=c(1, 3))

Solution

  • The original code had several issues:

    Fixing all this, we come to the solution below. It runs through and fits somehow, but it can still be improved. The warnings may be ignored for now. The reason is, that parameters exceed the allowed range. This can of course be fixed but would be another question.

    library(deSolve)
    library(ggplot2) 
    library(minpack.lm)
    library(reshape2)
    
    data <- data.frame(
      time = c(0, 3, 5, 8, 9.5, 11.5, 14, 16, 18, 20, 25, 27),
      X = c(0.0904, 0.1503, 0.2407, 0.3864, 0.5201, 0.6667, 0.8159, 0.9979, 1.0673, 1.1224, 1.1512, 1.2093),
      S = c(9.0115, 8.8088, 7.9229, 7.2668, 5.3347, 4.911, 3.5354, 1.4041, 0, 0, 0, 0),
      P = c(0.0151, 0.0328, 0.0621, 0.1259, 0.2949, 0.3715, 0.4199, 0.522, 0.5345, 0.6081, 0.07662, 0.7869)
    )
    
    Monod <- function(t, c, parms) {
      k1 <- parms$k1 # mumax
      k2 <- parms$k2 # Ks
      k3 <- parms$k3 # Y_(X/S)
      k4 <- parms$k4 # alpha
      k5 <- parms$k4 # beta
      r <- numeric(length(c))
      r[1] <-  k1 * c["S"] * c["X"] / (k2 + c["S"])
      r[2] <- -k1 * c["S"] * c["X"] / ((k2 + c["S"]) * k3)
      r[3] <-  k4 * r[1] + k5 * c["X"]
      return(list(r))       
    }
    
    cinit <- c(X = data[1, 2], S = data[1, 3], P = data[1, 4])
    t <- seq(0, 27, 1)
    parms <- list(k1=0.1, k2=5, k3=1, k4=0.08, k5 = 0.005)
    
    out <- ode(y=cinit, times=t, func = Monod, parms=parms)
    plot(out)
    
    residuals <- function(parms){
      cinit <- c(X = data[1, 2], S = data[1, 3], P = data[1, 4] )
      
      # time points for which conc is reported including the points where data is available
      t <- c(seq(0, 27, 1), data$t)
      t <- sort(unique(t))
      
      # parameters from the parameter estimation routine:
      k1 <- parms[1]
      k2 <- parms[2]
      k3 <- parms[3]
      k4 <- parms[4]
      
      # solve ODE for a given set of parameters
      out <- ode(y = cinit, times = t, func = Monod, parms = list( k1 = k1, k2 = k2, k3 = k3, k4 = k4) )
      
      # Filter data that contains time points where data is available:
      out_Monod <- data.frame(out)
      out_Monod <- out_Monod[out_Monod$t %in% data$t,]
      
      # Evaluate predicted vs experimental residual
      pred_Monod <- melt(out_Monod, id.var="time", variable.name="Substance", value.name="conc")
      exp_Monod  <- melt(data, id.var="time", variable.name="Substance", value.name="conc")
      residuals  <- pred_Monod$conc - exp_Monod$conc
      
      # return predicted vs experimental residual
      return(residuals)
    }
    
    # initial guess for parameters
    parms <- c(k1=0.5, k2=6.5, k3=0.2, k4=1.2)
    fitval <- nls.lm(par=parms, fn=residuals)
    summary(fitval)
    
    out <- ode(y=cinit, times=seq(min(data$time), max(data$time)), func = Monod, parms=as.list(coef(fitval)))
    plot(out, obs=data, mfrow=c(1, 3))
    

    fitted model