-
-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement spatial ODE simulations #571
Conversation
Cool! Will give a review tomorrow! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with merging something like this, though I'd prefer to drop the Symbol
usage and make SpatialReaction
s look more like normal Reaction
s in their interface.
But I think we will need to iterate on this interface over time to get it both performant and convenient to use.
@ChrisRackauckas any thoughts?
# A set of spatial reactions (denoting interaction between comaprtments). | ||
# A network of compartments (a meta graph that can contain some additional infro for each compartment). | ||
# The lattice is a DiGraph, normals graphs are converted to DiGraphs (with one edge in each direction). | ||
struct LatticeReactionSystem # <: MT.AbstractTimeDependentSystem # Adding this part messes up show, disabling me from creating LRSs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Define a custom show
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, yeah, that makes sense.
kwargs...) | ||
@unpack rs, spatial_reactions, lattice = lrs | ||
|
||
spatial_params = unique(getfield.(spatial_reactions, :rate)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, why not use symbolic variables here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to use symbolic variables, but was a bit uncertain of the proper way to use them here. Also, given that we don't actually use MTK I presume there was no advantage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The advantage is consistency with how non-spatial Catalyst works and with MTK (particularly since we will hopefully be able to switch to MTK/Symbolics for the code generation at some point). Also, wouldn't we potentially want to allow more complicated rate expressions at some point that are symbolic expressions?
Basically we should use symbolic variables/parameters as the standard objects -- Symbol
s in parameter mappings and initial conditions are just provided as a convenience (and should be the only place we currently use Symbol
s in the non-spatial code).
@unpack rs, spatial_reactions, lattice = lrs | ||
|
||
spatial_params = unique(getfield.(spatial_reactions, :rate)) | ||
pV_in, pE_in = split_parameters(p, spatial_params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Longer-term this interface will probably need changes. It is pretty common that transport rates for diffusion all have the form D_S k_{i,j}
for D_S
the diffusivity of a species, and the k_{i,j}
the same across all species. As such, we'd want to avoid users having to essentially instantiate these matrices for every species to save memory (i.e. we'd want to support different forms for these rate structures as JumpProcesses attempts to do). But that is an optmization we can work towards later on as we refine the interace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I fully follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now the parameter interface requires a user to give the full set of weights for all spatial transitions for each species across the graph edges right? In many cases though, a transition from node i to node j has the form I wrote above, where there is a species-dependent (spatially-constant) diffusion rate that multiplies a species-independent transition rate k_{i,j}
. We can discuss more on Monday if that is still confusing!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think I get it. Basically, typically there are N+M diffusion parameters (N is the number of species and M is the number of edges on the lattice). Then the diffusion rate along an edge is the product of a parameter for the species and a parameter for the edge.
The current interface doesn't directly permit for this. You could declare NxM different parameters, and set their values to follow this rule. However, a direct way to do this would be convenient?
I think this should be doable. I already implemented (for the old spatial reaction, but will re-implement) a DiffusionReaction
function, that creates a diffusion reaction without lots of empty []
having to be passed to the SpatialReaction
. I could expand on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can follow up on this after thinking a bit more about how to specify the full spatial parameters anyways, so what you have for now is probably fine for this PR. (In an effort to avoid it growing really big and/or sitting forever.)
src/spatial_reaction_network.jl
Outdated
matrix_form(input::Vector, n, index_dict) = matrix_form(map(i -> (i[2] isa Vector) ? i[1] => i[2] : i[1] => fill(i[2], n), input), n, index_dict); | ||
|
||
# Creates a function for simulating the spatial ODE with spatial reactions. | ||
function build_f(lrs::LatticeReactionSystem, u_idxs::Dict{Symbol,Int64}, pE_idxes::Dict{Symbol,Int64}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since your generated function is capturing variables, why not use a functor (i.e. make a structure, SpatialReactionODEFun
that stores the variables and then create a general functor SpatialReactionODEFun(du, u, p, t)
)? This should avoid any need to dynamically generate a function right? Then you just return an instance of the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's probably the way to go, I'm just unfamiliar with actually using the approach and its advantages,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects
mutable struct GraphODEFunWrapper
p1
p2
end
# in-place ODE derivative function
function (g::GraphODEFunWrapper)(du, u, p, t)
# can now use g.p1 and g.p2
end
So you create a new GraphODEFunWrapper
to store the needed parameters that aren't in p
and return that to the user. It can still be called like a function in the ODE solvers.
src/spatial_reaction_network.jl
Outdated
end | ||
|
||
# Get the rate of a specific reaction. | ||
function get_rate(sr, pE, u_src, u_dst, u_idxs, pE_idxes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not reuse the current ODE rate generation code for non-spatial ReactionSystem
s?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will check if that works, good point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a look at this, but didn't really see how I could reuse it given that here we have two sets of substrates (one for each of the two neighbouring compartments.
Also, for normal ODEs we just generate a symbolic expression with oderatelaw
, while here we actually have to calculate a number to plug into our ODE function. Similarily I was a bit uncertain how to utilise symbolic rates in the SpatrialReaction, given that we at some point have to go back to just dealing with numbers, and have to get a value from the symbolic expression and the p
and u
matrices.
I had to revert to specifying things as |
That sounds like an issue with the |
Yeah, there's definitely something with the Right now I don't really use symbols on the inside either. However, given that parameter inputs are given on forms like:
each parameter has to have some kind of name/tag so that we can figure out which parameter should have which value from the input. Internally, we use |
Gone through the code, and now the profiler looks good. Still a problem that: using Catalyst, OrdinaryDiffEq, Graphs
rs = @reaction_network begin
A, ∅ → X
1, 2X + Y → 3X
B, X → Y
1, X → ∅
end A B
srs = [SpatialReaction(:D, ([:X], []), ([], [:X]), ([1], Vector{Int64}()), (Vector{Int64}(), [1]))]
lattice = grid([25, 10])
lrs = LatticeReactionSystem(rs, srs, lattice);
u0_in = [:X => 10 * rand(nv(lattice)), :Y => 10 * rand(nv(lattice))]
tspan = (0.0, 100.0)
p_in = [:A => 1.0, :B => 4.0, :D => 0.2]
oprob = ODEProblem(lrs, u0_in, tspan, p_in)
@time sol = solve(oprob, Tsit5());
@time sol = solve(oprob, FBDF()); e.g. the |
Preconditioned GMRES? |
I can run GMRES (small speedup), but haven't managed to get preconditioners working yet. However, GMRES is Jacobian free-right? And currently the problem is that getting a Jacobian scales poorly with the problem size? I don't think I had this problem when I created the problem through MTK, but MTK automatically produces an jacobian for you. However, it should be possible to create the Jacobian at problem creation from the input (just as the Does this reasoning make sense? In which case trying to generate a Jacobian when the |
GMRES is Jacobain-free. Preconditioners need Jacobians but if you just do |
Figured so. But since we are not using MTK then we would have to create the Jacobian from scratch anyway? Which would mean that there is no way around writing a function that can generate the jacobian. |
No, you'd just need to determine the sparsity pattern. |
For the preconditioner? Got it. |
Ok, we are back in action. In addition to, for a
There are still some minor tuning to do, as well as some deciding how to set up the inputs. However, the big hurdle should be done now. |
17f64f4
to
1b2e152
Compare
Since it is a big thing, I simply reuploaded it as #644, trying to ensure that it is properly built on the latest Catalyst version. |
New implementation of spatial ODE simulations. Instead of being based on MTK we now just write a new f function for our given spatial system, that loops through all compartments and connections. The advantage of this is that we no longer need to store a big equation, which helps with simulations over a large number of cells.
When making a spatial simulation:
u0
is a NxM matrix, where N is the number of species in the (non-spatial) reaction network, and M is the number of compartments.p
is a tupple(pV,pE)
,pV
is a LxM matrix, where L is the number of parameters in the (non-spatial) reaction network, and M is the number of compartments.pE
is a RxS matrix, where R is the number of parameters tied to spatial reactions, and S is the number of connections.Sample code:
Plotting is not added to Catalyst right now, but running this code:
enable us to create a plot through:
which show spatial effects in the Brusselator:
https://user-images.githubusercontent.com/18099310/205381134-b72f02bd-c24a-4264-a238-41d0d8fa2420.mp4
(I've never used Makie before, so still some things to iron out there to make the animation prettier)