This solved the problem! Thank you very much. It turns out the main problem was with Pluto. The following example worked for me in VSCode:
using Catalyst
using Symbolics
using DifferentialEquations
#Define reaction system
push_pull=@reaction_network begin
(k_a, k_d), X + K <--> XK
k_cat, XK --> X⁺ + K
(k_a, k_d), X⁺ + P <--> X⁺P
k_cat, X⁺P --> X + P
end
#Convert to ODESystem:
osys=convert(ODESystem, push_pull)
#Access equations
eqs=equations(osys)
#Get relevant symbolic variables
t = Catalyst.default_t()
D = Catalyst.default_time_deriv()
@unpack X, K, XK, X⁺, P, X⁺P = osys
#Find equations for the intermediates
XK_eq = first(filter(eq -> isequal(D(XK), eq.lhs), eqs))
X⁺P_eq = first(filter(eq -> isequal(D(X⁺P), eq.lhs), eqs))
#Find the equation for X⁺
X⁺_eq = first(filter(eq -> isequal(D(X⁺), eq.lhs), eqs))
#tQSSA: Define hat variable (hat_X⁺=X⁺+X⁺P), total X concentration and their substitution into the original ODEs. Also define enzyme concentrations in terms of their total concentration.
@variables hat_X⁺(t)
@parameters Xᵀ Kᵀ Pᵀ
hat_substitution=Dict(
X⁺ => hat_X⁺-X⁺P,
X => Xᵀ-hat_X⁺-XK,
K => Kᵀ-XK,
P => Pᵀ-X⁺P
)
#substituting in the tQSSA variables
XK_eq_sub = substitute(XK_eq.rhs, hat_substitution)
X⁺P_eq_sub = substitute(X⁺P_eq.rhs, hat_substitution)
X⁺_eq_sub = substitute(X⁺_eq.rhs, hat_substitution)
#Setting up the DAE:
DAE=[
#ODE
D(hat_X⁺)~simplify(X⁺_eq_sub+X⁺P_eq_sub)
#Algebraic equations
0~XK_eq_sub
0~X⁺P_eq_sub
]
#Solving the DAE:
@parameters k_a, k_d, k_cat
@named tQSSA_sys=ODESystem(DAE, t, [hat_X⁺, XK, X⁺P], [k_a, k_d, k_cat, Xᵀ, Kᵀ, Pᵀ])
tQSSA_sys=structural_simplify(tQSSA_sys)
# Define initial conditions
X⁺0 = Dict(
hat_X⁺ => 0.0
)
complexes0 = Dict(
XK => 0.0,
X⁺P => 0.0
)
# Define parameters
p = [
k_a => 1.0,
k_d => 1.0,
k_cat => 1.0,
Xᵀ => 1.0,
Kᵀ => 1.0,
Pᵀ => 1.0
]
# Define time span
tspan = (0.0, 10.0)
# Solve the DAE
tQSSA_prob = ODAEProblem(tQSSA_sys, X⁺0, tspan, p, guesses=complexes0)
tQSSA_sol = solve(prob, Rosenbrock23())
# To see accuracy, also solve the original ODE system
function getdictval(dict, key)
#Gets the value corresponding to a key in a dictionary, even if the key is symbolic.
return filter(el -> isequal(el.first, key), dict)[1].second
end
X0 = Dict(
X => getdictval(p, Xᵀ),
K => getdictval(p, Kᵀ),
XK => 0.0,
X⁺ => 0.0,
P => getdictval(p, Pᵀ),
X⁺P => 0.0
)
mass_action_prob = ODEProblem(push_pull, X0, tspan, p)
mass_action_sol = solve(mass_action_prob, Tsit5())
#Plotting
using Plots
plt=plot( title="Mass Action vs tQSSA", xlabel="Time", ylabel="X⁺", legend=:bottomright)
plot!(mass_action_sol.t, mass_action_sol[:X⁺], label="Mass Action")
plot!(tQSSA_sol.t, reduce(vcat, getindex.(tQSSA_sol.u, 1,:).-getindex.(tQSSA_sol.u, 2,:)), label="tQSSA")
display(plt)
I believe Torkel is working on an easier example, that is probably also much more elegant. For the mean time, this works for me. Thank you both for the help!