Catalyst.jl
Catalyst.jl copied to clipboard
Implement equations in DSL planning
Ok, I hope to start on this soon, just wanted to go through a few things before I begin implementation.
Currently, I plan to implement two different types of equations (over two separate PRs):
- Observables.
- Differential equations. Would any other cases make sense?
Observables
I think these are the easiest. We would add an option @observable to the DSL, and the syntax be something like:
@reaction_network begin
@observable Xtot = X1 + X2
...
end
Then, X (calculated according to the rule) would be made an observable of the system. Several observables could be computed in a block:
@reaction_network begin
@observables begin
Xtot = X1 + X2
Ytot = Y1 + Y2 + Y3
end
...
end
Considerations
- The current plan is to store these in the
observedfield. This has some other uses in MTK when it performsstructural_simplify. I would need to check that this is indeed ok. - The observables should not create equations in between each other (and each should only depend on the equations). It might be possible though to allow some simple inter-dependency like
@observables begin
Ptot = P1 + P2
prot_tot = Ptot + Q
end
- Observables could depend on species, parameters, and variables. What if an unknown symbol is encountered in the expression? We could either default to creating it as a parameter, or throwing an error. I think I would prefer the later, but for differential equations I would prefer the latter, and consistency is good. Generally I feel stronger about wanting to default to parameters for differential equations (than wanting to throw errors for observables).
Differential equations
The idea is to permit something like this:
@reaction_network begin
@equation D(V) ~ r*G # Here r could be a parameter and G a species.
...
end
Again, blocks of several equations could be created using
@equations begin
D(V) ~ r*G #
D(N) ~ r*e*G
end
Considerations
- The left-hand side would be forced to be a single variable and its differential.
- The left-hand side would be automatically be detected and created as a variable and added to the system (no
@variables V(t)needed). - It would automatically be assumed to be time-dependant (we could force something like
D(V(t)) ~ r*Gto make this explicit). - For the right-hand side expression, I would want to default to making undeclared component parameters (rather than requiring the user to declare these).
- Currently, no
@equationor@equationsmacros actually exist. So in actual code we would only runD(V) ~ r*Ginternally. Technically this mean we could use the term@diff_equationor@diff_eqor something. This would make it explicit that we are expecting a differential equation. - Here, the
Dvariable is reserved for a differential. It is possible to (internally) replace it with the full wordDifferentialin the code (mutatingD(V) ~ r*GtoDifferential(V) ~ r*Gbefore it is run. That way, we could still permitDto be a species/parameter/variable. - Technically, we could use syntax like
V' ~ r*Ginstead (or evenV' = r*G). While I would have preferred that, keeping as similar to modeling toolkit as possible does make sense.
Is there a reason to exclude algebraic equations?
Not really. Just that I have little experience with them, and afraid of what would happen if you defined something like
@reaction_network begin
@eqaution X = Y + 12
(p,d), 0 <--> X
(p,d), 0 <--> Y
end
which is non-solvable.
As long as stuff like this don't happen I guess it would be fine? However, I figured most (but not all) algebraic equations people would want to use would be covered by the observables system.
Observables could depend on species, parameters, and variables. What if an unknown symbol is encountered in the expression? We could either default to creating it as a parameter, or throwing an error. I think I would prefer the later, but for differential equations I would prefer the latter, and consistency is good. Generally I feel stronger about wanting to default to parameters for differential equations (than wanting to throw errors for observables).
Some input on this, I think throwing an error might be nice. If the observable is used in parameter estimation, being able to detect non-dynamic parameters that only appear in the observable is beneficial (which I do not know is possible if you add them as a parameter), because for non-dynamic parameters we can efficiently compute the gradient given an ODE solution.
In this case, it might be possible to add this kind of information as metadata to these parameters?
As metadata in catalyst - or downstream packages?
ModelingToolkit (and in extent, Catalyst) variables and parameters have a metadata field where additional information on them can be stored (accessed through e.g. my_param.val.metadata, although each metadata typically has its own getters). We could store stuff like this (about a parameter) here.
Another possibility is that stuff like priors on a parameter's value (currently declared when a PEtabParameter is created) could be declared as parameter metadata (if desired) already when the Catalyst ReactionSystem is created. PEtab could then check this, and if it exist, use it.
Generally our goal should be to maintain consistency with the conventions of MTK as much as possible. This makes it easier for Catalyst users to use MTK and vice-versa.
Along those lines,
- Observables should just be equations of the form
A ~ B + Cand declared as such. I have to double check, but I believe this is how they are stored (i.e. they are assumed to define the value of something on the left side and are stored as MTK equations), and will presumably be what MTK would use if/when they add observables into their DSL. - Outside of reactions, for which we've already established our conventions, for all other new blocks we should require pre-declaring any parameters, species, or variables that are used in an expression. So this would apply to observables, equations, events, compounds, etc. If the block is defining something, such as an observable
AviaA ~ B + Cthen we could allowAto be defined via the block, however,BandCshould need an explicit pre-declaration. Note that some care will then have to be taken to figure out ifAis a species or variable. (edit: Maybe we should just make all observables a variable, they wouldn't be included in the species list anyways.) - By default, the derivative operator
D(A)should be with respect toDEFAULT_IV, however, if a user manually specifies another defaultivas part of the DSL it should then be with respect to this iv. Likewise, we should ultimately give the ability for a user to define and use different derivative operators to support PDEs (but this can be a followup). We do currently support multiple ivs in Catalyst to enable conversion to PDESystems for MethodOfLines, so we don't want to introduce too many non-interoperable features in adding these blocks. @equationsshould allow algebraic equations too. I believeReactionSystemsupports this, but if it doesn't we should fix that up (it should be straightforward since we just forward them to the MTK system type). The notation for defining them should just be what MTK would use.D(A) ~ -k*Afor example or0 ~ -k*A.
- We should allow higher derivatives and any other equation types MTK allows, so really we just want to assume users have typed a valid MTK equation, and then pass it into the
ReactionSystemconstructor as part of the equation and reaction list.
- We should reuse as much as possible the
@equationsand other parsing code that has been put into MTK. This will make it easier for Catalyst to pickup any new functionality added to the MTK DSL and simplify what we have to maintain.
now works on master