Stochastic simulations#
Gillespie Algorithm#
using StatsBase ## Weights() and sample()
using Plots
using Interpolations
using Statistics ## mean()
using Random ## randexp()
using DisplayAs: PNG
Random.seed!(2024)
Random.TaskLocalRNG()
Stochastic chemical reaction: Gillespie Algorithm (direct method) Adapted from: Chemical and Biomedical Enginnering Calculations Using Python Ch.4-3
function ssa_direct(model, u0::AbstractArray, tend, p, stoich; tstart=zero(tend))
t = tstart ## Current time
ts = [t] ## Time points
u = copy(u0) ## Current state
us = copy(u) ## States over time
while t < tend
a = model(u, p, t) ## propensities
dt = randexp() / sum(a) ## Time step for the direct method
du = sample(stoich, Weights(a)) ## Choose the stoichiometry for the next reaction
u .+= du ## Update time
t += dt ## Update time
us = [us u] ## Append state
push!(ts, t) ## Append time point
end
us = collect(us') ## Transpose to make columns as state variables, rows as observations
return (t = ts, u = us)
end
ssa_direct (generic function with 1 method)
Stochastic chemical reaction: Gillespie Algorithm (first reaction method) Adapted from: Chemical and Biomedical Enginnering Calculations Using Python Ch.4-3
function ssa_first(model, u0, tend, p, stoich; tstart=zero(tend))
t = tstart ## Current time
ts = [t] ## Time points
u = copy(u0) ## Current state
us = copy(u) ## States over time
while t < tend
a = model(u, p, t) ## propensities
dts = randexp(length(a)) ./ a ## time scales of all reactions
i = argmin(dts) ## Choose which reaction to occur
dt = dts[i]
du = stoich[i]
u .+= du ## Update state
t += dt ## Update time
us = [us u] ## Append state variable to record
push!(ts, t) ## Append time point to record
end
us = collect(us') ## Make column as variables, rows as observations
return (t = ts, u = us)
end
ssa_first (generic function with 1 method)
Propensity model for this example reaction. Reaction of A <-> B with rate constants k1 & k2
model(u, p, t) = [p.k1 * u[1], p.k2 * u[2]]
model (generic function with 1 method)
parameters = (k1=1.0, k2=0.5)
(k1 = 1.0, k2 = 0.5)
Stoichiometry for each reaction
stoich=[[-1, 1], [1, -1]]
2-element Vector{Vector{Int64}}:
[-1, 1]
[1, -1]
Initial conditions (Usually discrete values)
u0 = [200, 0]
2-element Vector{Int64}:
200
0
Simulation time
tend = 10.0
10.0
Solve the problem using both direct and first reaction method
@time soldirect = ssa_direct(model, u0, tend, parameters, stoich)
@time solfirst = ssa_first(model, u0, tend, parameters, stoich)
0.436588 seconds (518.98 k allocations: 42.560 MiB, 98.96% compilation time)
0.257400 seconds (368.71 k allocations: 32.579 MiB, 20.32% gc time, 99.33% compilation time)
(t = [0.0, 0.006874672879606227, 0.0070050915014185, 0.01329115182676454, 0.015335530116774018, 0.01942319769041779, 0.021346508038833215, 0.02345030020299408, 0.025497753122539372, 0.02734483697039726 … 9.937773197389458, 9.938360255993572, 9.964450740930666, 9.96564917153764, 9.975440668199402, 9.978594329657025, 9.982318425245289, 9.987213207313044, 9.995833233887383, 10.024199143896238], u = [200 0; 199 1; … ; 75 125; 74 126])
Plot the solution from the direct method
plot(soldirect.t, soldirect.u,
xlabel="time", ylabel="# of molecules",
title = "SSA (direct method)", label=["A" "B"]) |> PNG
Plot the solution by the first reaction method
plot(solfirst.t, solfirst.u,
xlabel="time", ylabel="# of molecules",
title = "SSA (1st reaction method)", label=["A" "B"]) |> PNG
Running 50 simulations
numRuns = 50
sols = map(1:numRuns) do _
ssa_direct(model, u0, tend, parameters, stoich)
end;
A interpolation function for each solution
itpsA = map(sols) do sol
linear_interpolation(sol.t, sol.u[:, 1], extrapolation_bc = Line())
end;
itpsB = map(sols) do sol
linear_interpolation(sol.t, sol.u[:, 2], extrapolation_bc = Line())
end;
Calculate average A and B levels in the ensemble.
a_avg(t) = mean(i->i(t), itpsA)
b_avg(t) = mean(i->i(t), itpsB)
b_avg (generic function with 1 method)
Plot the soluton
fig1 = plot(xlabel="Time", ylabel="# of molecules", title = "SSA (direct method) ensemble")
for sol in sols
plot!(fig1, sol.t, sol.u, linecolor=[:blue :red], linealpha=0.05, label=false)
end
fig1 |> PNG
Plot averages
plot!(fig1, a_avg, 0.0, tend, linecolor=:black, linewidth=3, linestyle = :solid, label="Avarage [A]") |> PNG
plot!(fig1, b_avg, 0.0, tend, linecolor=:black, linewidth=3, linestyle = :dash, label="Avarage [B]") |> PNG
fig1 |> PNG
Using Catalyst (recommended)#
Catalyst.jl is a domain-specific language (DSL) package to solve law of mass action problems.
using Catalyst
using JumpProcesses
using Plots
rn = @reaction_network begin
k1, A --> B
k2, B --> A
end
The system with integer state variables belongs to DiscreteProblem
. A DiscreteProblem
could be further dispatched into other types of problems, such as ODEProblem
, SDEProblem
, and JumpProblem
.
params = [:k1 => 1.0, :k2 => 0.5]
u0 = [:A => 200, :B => 0]
tend = 10.0
dprob = DiscreteProblem(rn, u0, (0.0, tend), params)
DiscreteProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 10.0)
u0: 2-element Vector{Float64}:
200.0
0.0
In this case, we would like to solve a JumpProblem
using Gillespie’s Direct stochastic simulation algorithm (SSA).
jumpProb = JumpProblem(rn, dprob, Direct())
sol = solve(jumpProb, SSAStepper())
plot(sol) |> PNG
Parallel ensemble simulation
ensprob = EnsembleProblem(jumpProb)
sim = solve(ensprob, SSAStepper(), EnsembleThreads(); trajectories=50)
EnsembleSolution Solution of length 50 with uType:
SciMLBase.ODESolution{Float64, 2, Vector{Vector{Float64}}, Nothing, Nothing, Vector{Float64}, Nothing, Nothing, SciMLBase.DiscreteProblem{Vector{Float64}, Tuple{Float64, Float64}, true, ModelingToolkit.MTKParameters{Vector{Float64}, Tuple{}, Tuple{}, Tuple{}, Tuple{}}, SciMLBase.DiscreteFunction{true, true, SciMLBase.DiscreteFunction{true, SciMLBase.FullSpecialize, SciMLBase.var"#240#241", Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing, Nothing}, Nothing, ModelingToolkit.ObservedFunctionCache{ModelingToolkit.JumpSystem{RecursiveArrayTools.ArrayPartition{Any, Tuple{Vector{JumpProcesses.MassActionJump}, Vector{JumpProcesses.ConstantRateJump}, Vector{JumpProcesses.VariableRateJump}, Vector{Symbolics.Equation}}}}}, ModelingToolkit.JumpSystem{RecursiveArrayTools.ArrayPartition{Any, Tuple{Vector{JumpProcesses.MassActionJump}, Vector{JumpProcesses.ConstantRateJump}, Vector{JumpProcesses.VariableRateJump}, Vector{Symbolics.Equation}}}}, Nothing}, Base.Pairs{Symbol, Union{}, Tuple{}, @NamedTuple{}}}, JumpProcesses.SSAStepper, SciMLBase.ConstantInterpolation{Vector{Float64}, Vector{Vector{Float64}}}, SciMLBase.DEStats, Nothing, Nothing, Nothing, Nothing}
plot(sim, alpha=0.1, color=[:blue :red]) |> PNG
summ = EnsembleSummary(sim, 0:0.1:10)
plot(summ,fillalpha=0.5) |> PNG
See also the JumpProcesses.jl docs about discrete stochastic examples.
High-level solutions using
Catalyst.jl
and low-level solutions defining the jumps directly.Coupling stochastic discrete jumping and ODEs.
RegularJumps
using a more efficient tau-leaping method.More solvers for discrete stochastic simulations.
This notebook was generated using Literate.jl.