Skip to content

Solution of SteadyStateProblem is extremely slow #409

@SmearingMap

Description

@SmearingMap

I switched some code over from DiffEqBiological to Catalyst and noticed that Catalyst was much, much slower.
The problem here is to determine steady states for a MAPK cascade for values of the input E1 that range over a few orders of magnitude (this is the same problem and with similar solved in this 1996 paper, so we're not talking about anything that should cause major problems at the solver level). Using DiffEqBiological I was able to solve this problem for 10,000 points spanning 5 orders of magnitude in under a minute (I never bothered timing, because it was fast enough not to be an issue). When I switched over to Catalyst, trying to do the same thing was not feasible at all.

This code shows the issue. When I ran it, I noticed a couple of things:

  1. Making multiple problems is very slow. Using btime tells me that it takes about 200 milliseconds to make each problem, and that doubling the number of problems doubles the amount of time taken.
  2. It only takes 35 milliseconds to solve 50 problems, but a lot more than that to solve 100. I started running this several minutes ago, and btime still hasn't told me how long it takes to solve 100 problems.

Again, this problem was trivial before switching over from DiffEqBiological to Catalyst, so it seems like it has to be an issue with Catalyst, since I was using the same version of DifferentialEquations along with DiffEqBiological.

using Catalyst
using DifferentialEquations
using BenchmarkTools

MAPK_cascade = @reaction_network begin
    
    # Raf phosphorylation
    a_1, Raf0 + E1 --> Raf0E1
    d_1, Raf0E1 --> Raf0 + E1
    k_1, Raf0E1 --> Raf1 + E1
    
    # Raf de-phosphorylation
    a_2, Raf1 + E2 --> Raf1E2
    d_2, Raf1E2 --> Raf1 + E2
    k_2, Raf1E2 --> Raf0 + E2
    
    # First MEK phosphorylation
    a_3, Raf1 + Mek0 --> Raf1Mek0
    d_3, Raf1Mek0 --> Raf1 + Mek0
    k_3, Raf1Mek0 --> Raf1 + Mek1
    
    # First MEK de-phosphorylation
    a_4, Mek1 + MekF --> Mek1MekF
    d_4, Mek1MekF --> Mek1 + MekF
    k_4, Mek1MekF --> Mek0 + MekF
    
    # Second MEK phosphorylation
    a_5, Raf1 + Mek1 --> Raf1Mek1
    d_5, Raf1Mek1 --> Raf1 + Mek1
    k_5, Raf1Mek0 --> Raf1 + Mek2
    
    # Second MEK de-phosphorylation
    a_6, Mek2 + MekF --> Mek2MekF
    d_6, Mek2MekF --> Mek2 + MekF
    k_6, Mek2MekF --> Mek1 + MekF
    
    # First ERK phosphorylation
    a_7, Mek2 + Erk0 --> Mek2Erk0
    d_7, Mek2Erk0 --> Mek2 + Erk0
    k_7, Mek2Erk0 --> Mek2 + Erk1
    
    # First ERK de-phosphorylation
    a_8, Erk1 + ErkF --> Erk1ErkF
    d_8, Erk1ErkF --> Erk1 + ErkF
    k_8, Erk1ErkF --> Erk0 + ErkF
    
    # Second ERK phosphorylation
    a_9, Mek2 + Erk1 --> Mek2Erk1
    d_9, Mek2Erk1 --> Mek2 + Erk1
    k_9, Mek2Erk1 --> Mek2 + Erk2
    
    # Second ERK de-phosphorylation
    a_10, Erk2 + ErkF --> Erk2ErkF
    d_10, Erk2ErkF --> Erk2 + ErkF
    k_10, Erk2ErkF --> Erk1 + ErkF
end a_1 d_1 k_1 a_2 d_2 k_2 a_3 d_3 k_3 a_4 d_4 k_4 a_5 d_5 k_5 a_6 d_6 k_6 a_7 d_7 k_7 a_8 d_8 k_8 a_9 d_9 k_9 a_10 d_10 k_10


function unbound_initial_condition(E1, E2,
                                   Raf,
                                   Mek, MekF,
                                   Erk, ErkF
                                    )
    #=
    Set all complex concentrations to zero initially.
    Setting entries is hardcoded to sidestep dictionary issue.
    =#
    x0 = zeros(length(MAPK_cascade.states))
    x0[1] = Raf
    x0[2] = E1
    x0[5] = E2
    x0[7] = Mek
    x0[10] = MekF
    x0[15] = Erk
    x0[18] = ErkF
    x0
end

function unbound_initial_condition(E1::Number)
    #=
    Set only E1 and fill in the rest with default values
    =#
    unbound_initial_condition(E1, 
        DEFAULT_CONCENTRATIONS[2:length(DEFAULT_CONCENTRATIONS)]...
    )
end

#= Setting default rates to obtain K_m = 100 nanomolar
   and rate constants to be order-of-magnitude consistent
   with Table 1 of the review by Piala 2014.
   Note: Ferrell 1996 uses K_m = 300 nanomolar for all
   Michaelis constants.
=#

DEFAULT_RATES = repeat([100.0, 1.0, 9.0], 10)


DEFAULT_CONCENTRATIONS = [
                              0.001, #E1
                              0.001, #E2
                              0.003, #Raf
                              1.2,   #Mek
                              0.001, #MekF
                              1.2,   #Erk
                              0.1    #ErkF
                            ];

xs = collect(10.0^i for i in -7:0.1:-2)

x0s = unbound_initial_condition.(xs)

#=
Define functions needed for the steady state response function
=#
odesys = convert(ODESystem, MAPK_cascade)
make_problem(x0) = SteadyStateProblem(odesys, x0, DEFAULT_RATES)
solve_problem(p) = solve(p, DynamicSS(Rodas5()); abstol=1e-4, reltol=1e-3)
response_function(x) = solve_problem(make_problem(unbound_initial_condition(x)))


println("Time to make 50 problems:")
@btime make_problem.(x0s)
problems = make_problem.(x0s)
println("Time to solve 50 problems:")
@btime solve_problem.(problems)


#= Increasing the number of problems seems to increase the time faster than linearly. This could be a stiffness problem, but I was able to solve this problem on 10,000 points in just a few seconds with DiffEqBiological.
=#
 
xs = collect(10.0^i for i in -7:0.05:2)
x0s = unbound_initial_condition.(xs);

println("Time to make 100 problems:")
@btime make_problem.(x0s)
problems = make_problem.(x0s)
println("Time to solve 100 problems:")
@btime solve_problem.(problems)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions