Search code examples
juliamathematical-optimizationleast-squaresdifferentialequations.jl

Parameter estimation of multiple datasets in julia DifferentialEquations


I have been looking and I could not find a direct way of using the DifferentialEquations parameter estimation in julia to fit multiple datasets. So, let's say we have this simple differential equation with two parameters:

f1 = function (du,u,p,t)
  du[1] = - p[1]*p[2] * u[1]
end

We have experimental datasets of u[1] vs t. Each dataset has a different value of p[2] and/or different initial conditions. p[1] is the parameter we want to estimate. I can do this by solving the differential equation in a for loop that iterates over the different initial conditions and p[2] values, storing the solutions in an array and create a loss-function against the experimental data. I wonder if there is a way of doing this in fewer lines of code, using, for instance,DiffEqBase.problem_new_parameters to set the conditions of each dataset. This is a very common situation when fitting models to experimental data but I could not find a good example in the documentation.

Thank you in advance,

Best regards

EDIT 1

The case expressed above is just a simplified example. To make it a practical case we could create some fake experimental data from the following code:

using DifferentialEquations

# ODE function
f1 = function (du,u,p,t)
  du[1] = - p[1]*p[2] * u[1]
end

# Initial conditions and parameter values.
# p1 is the parameter to be estimated.
# p2 and u0 are experimental parameters known for each dataset.
u0 = [1.,2.]
p1 = .3
p2 = [.1,.2]
tspan = (0.,10.)
t=linspace(0,10,5)


# Creating data for 1st experimental dataset.
## Experimental conditions: u0 = 1. ; p[2] = .1
prob1 = ODEProblem(f1,[u0[1]],tspan,[p1,p2[1]])
sol1=solve(prob1,Tsit5(),saveat=t)

# Creating data for 2nd experimental dataset.
## Experimental conditions: u0 = 1. ; p[2] = .2
prob2 = ODEProblem(f1,[u0[1]],tspan,[p1,p2[2]])
sol2=solve(prob2,Tsit5(),saveat=t)

# Creating data for 3rd experimental dataset.
## Experimental conditions: u0 = 2. ; p[2] = .1
prob3 = ODEProblem(f1,[u0[2]],tspan,[p1,p2[1]])
sol3=solve(prob3,Tsit5(),saveat=t)

sol1, sol2 and sol3 are now our experimental data, each dataset using a different combination of initial conditions and p[2] (which represents some experimental variable (e.g., temperature, flow...)

The objective is to estimate the value of p[1] using the experimental data sol1, sol2 and sol3 letting DiffEqBase.problem_new_parameters or another alternative iterate over the experimental conditions.


Solution

  • What you can do is create a MonteCarloProblem that solves all three of the problems at once:

    function prob_func(prob,i,repeat)
      i < 3 ? u0 = [1.0] : u0 = [2.0]
      i == 2 ? p = (prob.p[1],0.2) : p = (prob.p[1],0.1)
      ODEProblem{true}(f1,u0,(0.0,10.0),p)
    end
    prob = MonteCarloProblem(prob1,prob_func = prob_func)
    
    @time sol = solve(prob,Tsit5(),saveat=t,parallel_type = :none,
                      num_monte = 3)
    

    Then create a loss function that compares each of the solutions against the 3 datasets and adds their loss together.

    loss1 = L2Loss(t,data1)
    loss2 = L2Loss(t,data2)
    loss3 = L2Loss(t,data3)
    loss(sol) = loss1(sol[1]) + loss2(sol[2]) + loss3(sol[3])
    

    Finally, you need to tell it how to relate the optimization parameter(s) to the problem it's solving. Here, our MonteCarloProblem holds a prob that it's pulling p[1] from whenever it's generating a problem. The value that we want to optimize is that p[1], so:

    function my_problem_new_parameters(prob,p)
      prob.prob.p[1] = p[1]
      prob
    end
    

    Now our objective is exactly those pieces together:

    obj = build_loss_objective(prob,Tsit5(),loss,
                               prob_generator = my_problem_new_parameters,
                               num_monte = 3,
                               saveat = t)
    

    Now let's throw that to Optim.jl's Brent method:

    using Optim
    res = optimize(obj,0.0,1.0)
    
    Results of Optimization Algorithm
     * Algorithm: Brent's Method
     * Search Interval: [0.000000, 1.000000]
     * Minimizer: 3.000000e-01
     * Minimum: 2.004680e-20
     * Iterations: 10
     * Convergence: max(|x - x_upper|, |x - x_lower|) <= 2*(1.5e-08*|x|+2.2e-16): true
     * Objective Function Calls: 11
    

    It found that the overall best value is 0.3 which is the parameter we used to generate the data.

    Here's the code in full:

    using DifferentialEquations
    
    # ODE function
    f1 = function (du,u,p,t)
      du[1] = - p[1]*p[2] * u[1]
    end
    
    # Initial conditions and parameter values.
    # p1 is the parameter to be estimated.
    # p2 and u0 are experimental parameters known for each dataset.
    u0 = [1.,2.]
    p1 = .3
    p2 = [.1,.2]
    tspan = (0.,10.)
    t=linspace(0,10,5)
    
    # Creating data for 1st experimental dataset.
    ## Experimental conditions: u0 = 1. ; p[2] = .1
    prob1 = ODEProblem(f1,[u0[1]],tspan,[p1,p2[1]])
    sol1=solve(prob1,Tsit5(),saveat=t)
    data1 = Array(sol1)
    
    # Creating data for 2nd experimental dataset.
    ## Experimental conditions: u0 = 1. ; p[2] = .2
    prob2 = ODEProblem(f1,[u0[1]],tspan,[p1,p2[2]])
    sol2=solve(prob2,Tsit5(),saveat=t)
    data2 = Array(sol2)
    
    # Creating data for 3rd experimental dataset.
    ## Experimental conditions: u0 = 2. ; p[2] = .1
    prob3 = ODEProblem(f1,[u0[2]],tspan,[p1,p2[1]])
    sol3=solve(prob3,Tsit5(),saveat=t)
    data3 = Array(sol3)
    
    function prob_func(prob,i,repeat)
      i < 3 ? u0 = [1.0] : u0 = [2.0]
      i == 2 ? p = (prob.p[1],0.2) : p = (prob.p[1],0.1)
      ODEProblem{true}(f1,u0,(0.0,10.0),p)
    end
    prob = MonteCarloProblem(prob1,prob_func = prob_func)
    
    # Just to show what this looks like
    sol = solve(prob,Tsit5(),saveat=t,parallel_type = :none,
                num_monte = 3)
    
    loss1 = L2Loss(t,data1)
    loss2 = L2Loss(t,data2)
    loss3 = L2Loss(t,data3)
    loss(sol) = loss1(sol[1]) + loss2(sol[2]) + loss3(sol[3])
    
    function my_problem_new_parameters(prob,p)
      prob.prob.p[1] = p[1]
      prob
    end
    obj = build_loss_objective(prob,Tsit5(),loss,
                               prob_generator = my_problem_new_parameters,
                               num_monte = 3,
                               saveat = t)
    
    using Optim
    res = optimize(obj,0.0,1.0)