Skip to content

Commit 4860c72

Browse files
Merge pull request #289 from isaacsas/rx_rate_laws
add ODE ratelaw expressions
2 parents d47dac5 + d45e24a commit 4860c72

File tree

4 files changed

+154
-41
lines changed

4 files changed

+154
-41
lines changed

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1919
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
2020
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2121
TreeViews = "a2a6695c-b41b-5b7d-aed9-dbfdeacea5d7"
22+
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
2223
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
2324

2425
[compat]

src/ModelingToolkit.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using DiffEqBase, Distributed
44
using StaticArrays, LinearAlgebra, SparseArrays
55
using Latexify, Unitful, ArrayInterface
66
using MacroTools
7+
using UnPack: @unpack
78

89
using Base.Threads
910
import MacroTools: splitdef, combinedef, postwalk, striplines
Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,47 @@
1-
struct Reaction
1+
struct Reaction{S <: Variable, T <: Number}
22
rate
3-
reactants::Vector{Operation}
3+
substrates::Vector{Operation}
44
products::Vector{Operation}
5+
substoich::Vector{T}
6+
prodstoich::Vector{T}
7+
netstoich::Vector{Pair{S,T}}
8+
only_use_rate::Bool
59
end
610

11+
function Reaction(rate, subs, prods, substoich, prodstoich;
12+
netstoich=nothing, only_use_rate=false, kwargs...)
13+
14+
subsv = isnothing(subs) ? Vector{Operation}() : subs
15+
prodsv = isnothing(prods) ? Vector{Operation}() : prods
16+
ns = isnothing(netstoich) ? get_netstoich(subsv, prodsv, substoich, prodstoich) : netstoich
17+
Reaction(rate, subsv, prodsv, substoich, prodstoich, ns, only_use_rate)
18+
end
19+
20+
# three argument constructor assumes stoichiometric coefs are one and integers
21+
function Reaction(rate, subs, prods; kwargs...)
22+
23+
sstoich = isnothing(subs) ? Int[] : ones(Int,length(subs))
24+
pstoich = isnothing(prods) ? Int[] : ones(Int,length(prods))
25+
Reaction(rate, subs, prods, sstoich, pstoich; kwargs...)
26+
end
27+
28+
# calculates the net stoichiometry of a reaction as a vector of pairs (sub,substoich)
29+
function get_netstoich(subs, prods, sstoich, pstoich)
30+
# stoichiometry as a Dictionary
31+
nsdict = Dict{Variable,eltype(sstoich)}(sub.op => -sstoich[i] for (i,sub) in enumerate(subs))
32+
for (i,p) in enumerate(prods)
33+
coef = pstoich[i]
34+
prod = p.op
35+
@inbounds nsdict[prod] = haskey(nsdict, prod) ? nsdict[prod] + coef : coef
36+
end
37+
38+
# stoichiometry as a vector
39+
ns = [el for el in nsdict if el[2] != zero(el[2])]
40+
41+
ns
42+
end
43+
44+
745
struct ReactionSystem <: AbstractSystem
846
eqs::Vector{Reaction}
947
iv::Variable
@@ -13,59 +51,74 @@ struct ReactionSystem <: AbstractSystem
1351
systems::Vector{ReactionSystem}
1452
end
1553

16-
function ReactionSystem(eqs,iv,dvs,ps;
17-
systems = ReactionSystem[],
18-
name=gensym(:ReactionSystem))
19-
ReactionSystem(eqs,iv,convert.(Variable,dvs),convert.(Variable,ps),name,systems)
54+
function ReactionSystem(eqs, iv, species, params; systems = ReactionSystem[],
55+
name = gensym(:ReactionSystem))
56+
57+
ReactionSystem(eqs, iv, convert.(Variable,species), convert.(Variable,params),
58+
name, systems)
2059
end
2160

22-
# TODO: Make it do the combinatorics stuff
23-
reaction_expr(reactants) = *(reactants...)
61+
# Calculate the ODE rate law
62+
function oderatelaw(rx)
63+
@unpack rate, substrates, substoich, only_use_rate = rx
64+
rl = rate
65+
if !only_use_rate
66+
coef = one(eltype(substoich))
67+
for (i,stoich) in enumerate(substoich)
68+
coef *= factorial(stoich)
69+
rl *= isone(stoich) ? substrates[i] : substrates[i]^stoich
70+
end
71+
(!isone(coef)) && (rl /= coef)
72+
end
73+
rl
74+
end
2475

25-
function essemble_drift(rs)
26-
D = Differential(rs.iv())
76+
function assemble_drift(rs)
77+
D = Differential(rs.iv())
2778
eqs = [D(x(rs.iv())) ~ 0 for x in rs.states]
79+
species_to_idx = Dict((x => i for (i,x) in enumerate(rs.states)))
2880

29-
for rx in rs.eqs
30-
for reactant in rx.reactants
31-
i = findfirst(x->x == reactant.op,rs.states)
32-
eqs[i] = Equation(eqs[i].lhs,eqs[i].rhs - rx.rate * reaction_expr(rx.reactants))
33-
end
34-
35-
for product in rx.products
36-
i = findfirst(x->x == product.op,rs.states)
37-
eqs[i] = Equation(eqs[i].lhs,eqs[i].rhs + rx.rate * reaction_expr(rx.reactants))
81+
for rx in rs.eqs
82+
rl = oderatelaw(rx)
83+
for (spec,stoich) in rx.netstoich
84+
i = species_to_idx[spec]
85+
if iszero(eqs[i].rhs)
86+
signedrl = (stoich > zero(stoich)) ? rl : -rl
87+
rhs = isone(abs(stoich)) ? signedrl : stoich * rl
88+
else
89+
Δspec = isone(abs(stoich)) ? rl : abs(stoich) * rl
90+
rhs = (stoich > zero(stoich)) ? (eqs[i].rhs + Δspec) : (eqs[i].rhs - Δspec)
91+
end
92+
eqs[i] = Equation(eqs[i].lhs, rhs)
3893
end
3994
end
4095
eqs
4196
end
4297

43-
function essemble_diffusion(rs)
98+
function assemble_diffusion(rs)
4499
eqs = Expression[Constant(0) for x in rs.states, y in rs.eqs]
100+
species_to_idx = Dict((x => i for (i,x) in enumerate(rs.states)))
45101

46102
for (j,rx) in enumerate(rs.eqs)
47-
for reactant in rx.reactants
48-
i = findfirst(x->x == reactant.op,rs.states)
49-
eqs[i,j] = -sqrt(rx.rate) * reaction_expr(rx.reactants)
50-
end
51-
52-
for product in rx.products
53-
i = findfirst(x->x == product.op,rs.states)
54-
eqs[i,j] = sqrt(rx.rate) * reaction_expr(rx.reactants)
103+
rlsqrt = sqrt(oderatelaw(rx))
104+
for (spec,stoich) in rx.netstoich
105+
i = species_to_idx[spec]
106+
signedrlsqrt = (stoich > zero(stoich)) ? rlsqrt : -rlsqrt
107+
eqs[i,j] = isone(abs(stoich)) ? signedrlsqrt : stoich * rlsqrt
55108
end
56109
end
57110
eqs
58111
end
59112

60113
function Base.convert(::Type{<:ODESystem},rs::ReactionSystem)
61-
eqs = essemble_drift(rs)
114+
eqs = assemble_drift(rs)
62115
ODESystem(eqs,rs.iv,rs.states,rs.ps,name=rs.name,
63116
systems=convert.(ODESystem,rs.systems))
64117
end
65118

66119
function Base.convert(::Type{<:SDESystem},rs::ReactionSystem)
67-
eqs = essemble_drift(rs)
68-
noiseeqs = essemble_diffusion(rs)
120+
eqs = assemble_drift(rs)
121+
noiseeqs = assemble_diffusion(rs)
69122
SDESystem(eqs,noiseeqs,rs.iv,rs.states,rs.ps,
70123
name=rs.name,systems=convert.(SDESystem,rs.systems))
71124
end

test/reactionsystem.jl

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,68 @@
1-
using ModelingToolkit
2-
3-
@parameters t a b c
4-
@variables x(t) y(t) z(t)
5-
rxs = [Reaction(a,[x],[y]),
6-
Reaction(b,[y],[x]),
7-
Reaction(c,[x,y],[x])]
8-
rs = ReactionSystem(rxs,t,[x,y],[a,b])
9-
sys = convert(ODESystem,rs)
10-
sys = convert(SDESystem,rs)
1+
using ModelingToolkit, LinearAlgebra, Test
2+
3+
@parameters t k[1:13]
4+
@variables A(t) B(t) C(t) D(t)
5+
rxs = [Reaction(k[1], nothing, [A]), # 0 -> A
6+
Reaction(k[2], [B], nothing), # B -> 0
7+
Reaction(k[3],[A],[C]), # A -> C
8+
Reaction(k[4], [C], [A,B]), # C -> A + B
9+
Reaction(k[5], [C], [A], [1], [2]), # C -> A + A
10+
Reaction(k[6], [A,B], [C]), # A + B -> C
11+
Reaction(k[7], [B], [A], [2], [1]), # 2B -> A
12+
Reaction(k[8], [A,B], [A,C]), # A + B -> A + C
13+
Reaction(k[9], [A,B], [C,D]), # A + B -> C + D
14+
Reaction(k[10], [A], [C,D], [2], [1,1]), # 2A -> C + D
15+
Reaction(k[11], [A], [A,B], [2], [1,1]), # 2A -> A + B
16+
Reaction(k[12], [A,B,C], [C,D], [1,3,4], [2, 3]), # A+3B+4C -> 2C + 3D
17+
Reaction(k[13]*A/(2+A), [A], nothing; only_use_rate=true) # A -> 0 with custom rate
18+
]
19+
rs = ReactionSystem(rxs,t,[A,B,C,D],k)
20+
odesys = convert(ODESystem,rs)
21+
sdesys = convert(SDESystem,rs)
22+
23+
# hard coded ODE rhs
24+
function oderhs(u,k,t)
25+
A = u[1]; B = u[2]; C = u[3]; D = u[4];
26+
du = zeros(eltype(u),4)
27+
du[1] = k[1] - k[3]*A + k[4]*C + 2*k[5]*C - k[6]*A*B + k[7]*B^2/2 - k[9]*A*B - k[10]*A^2 - k[11]*A^2/2 - k[12]*A*B^3*C^4/144 - k[13]*A/(2+A)
28+
du[2] = -k[2]*B + k[4]*C - k[6]*A*B - k[7]*B^2 - k[8]*A*B - k[9]*A*B + k[11]*A^2/2 - 3*k[12]*A*B^3*C^4/144
29+
du[3] = k[3]*A - k[4]*C - k[5]*C + k[6]*A*B + k[8]*A*B + k[9]*A*B + k[10]*A^2/2 - 2*k[12]*A*B^3*C^4/144
30+
du[4] = k[9]*A*B + k[10]*A^2/2 + 3*k[12]*A*B^3*C^4/144
31+
du
32+
end
33+
34+
# sde noise coefs
35+
function sdenoise(u,k,t)
36+
A = u[1]; B = u[2]; C = u[3]; D = u[4];
37+
G = zeros(eltype(u),length(k),length(u))
38+
z = zero(eltype(u))
39+
40+
G = [sqrt(k[1]) z z z;
41+
z -sqrt(k[2]*B) z z;
42+
-sqrt(k[3]*A) z sqrt(k[3]*A) z;
43+
sqrt(k[4]*C) sqrt(k[4]*C) -sqrt(k[4]*C) z;
44+
2*sqrt(k[5]*C) z -sqrt(k[5]*C) z;
45+
-sqrt(k[6]*A*B) -sqrt(k[6]*A*B) sqrt(k[6]*A*B) z;
46+
sqrt(k[7]*B^2/2) -2*sqrt(k[7]*B^2/2) z z;
47+
z -sqrt(k[8]*A*B) sqrt(k[8]*A*B) z;
48+
-sqrt(k[9]*A*B) -sqrt(k[9]*A*B) sqrt(k[9]*A*B) sqrt(k[9]*A*B);
49+
-2*sqrt(k[10]*A^2/2) z sqrt(k[10]*A^2/2) sqrt(k[10]*A^2/2);
50+
-sqrt(k[11]*A^2/2) sqrt(k[11]*A^2/2) z z;
51+
-sqrt(k[12]*A*B^3*C^4/144) -3*sqrt(k[12]*A*B^3*C^4/144) -2*sqrt(k[12]*A*B^3*C^4/144) 3*sqrt(k[12]*A*B^3*C^4/144);
52+
-sqrt(k[13]*A/(2+A)) z z z]'
53+
54+
return G
55+
end
56+
57+
# test by evaluating drift and diffusion terms
58+
p = rand(length(k))
59+
u = rand(length(k))
60+
t = 0.
61+
du = oderhs(u,p,t)
62+
G = sdenoise(u,p,t)
63+
sdesys = convert(SDESystem,rs)
64+
sf = SDEFunction{false}(sdesys, states(rs), parameters(rs))
65+
du2 = sf.f(u,p,t)
66+
@test norm(du-du2) < 100*eps()
67+
G2 = sf.g(u,p,t)
68+
@test norm(G-G2) < 100*eps()

0 commit comments

Comments
 (0)