-
-
Notifications
You must be signed in to change notification settings - Fork 79
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
[v14 ready] Improve noise scaling implementation #678
[v14 ready] Improve noise scaling implementation #678
Conversation
I haven't written tests. If this update looks good I will fix that and update the docs as well. |
Adding this macro seems like a good idea, and less clunkier than the current approach. So I'd be in favor of that. However, at a high level I don't like allowing mixed symbol and symbolic maps. It just worries me that it could lead to issues, and/or user expectations that won't be satisfied in other contexts. I also don't like having to slurp up If one is going to have to use symbolics for a bunch of the components one should just use them for all the components. The real issue you are encountering is that MTK doesn't yet support proper vector variables. If it did then you could just use using Catalyst, StochasticDiffEq, Plots
rn = @reaction_network begin
@noise_scaling_parameters η[1:4]
(p, d), 0 <--> X
(p, d), 0 <--> Y
end
rn = complete(rn) # this allows rn.symbolic in mappings
u0map = [rn.X => 10.0, rn.Y => 10.0]
tspan = (0.0, 10.0)
pmap = [rn.p => 10.0, rn.d => 1., rn.η[1]=>0.1, rn.η[2]=>0.1, rn.η[3]=>1., rn.η[4]=>1.]
prob = SDEProblem(rn, u0map, tspan, pmap)
sol = solve(prob, ImplicitEM())
plot(sol; ylimit=(0.0,20.0)) In fact, we should probably consider at some point changing the docs to use this approach of completing a model and |
If you prefer not to allow mixed symbols/symbolics I could change that. generally, I think I prefer using σV_network = @reaction_network begin
v0 + hill(σᵛ,v,K,n), ∅ → (σᵛ+A)
deg, (σᵛ,A,Aσᵛ) → ∅
(kB,kD), A + σᵛ ↔ Aσᵛ
L*kC*Aσᵛ, Aσᵛ ⟾ σᵛ
end
p = [σV_network.v0 = 0.1, σV_network.v = 2.5, σV_network.K = 60, n = 2, σV_network.kD = 5, σV_network.kB = 10, σV_network.kC = 0.05, σV_network.deg = 0.01; σV_network.L = 0.] becomes really messy. Either way, that's a discussion we can take another time. |
Yeah, why don't you get the functionality in here without changing the symbol mappings. Then we can discuss ways to improve their construction as a separate issue / PR. |
Also, and I haven't looked at your code closely, but can you make sure to still support the old style noise scaling for now, but add a deprecation warning that it will be removed for this new version in a future release? We should give an error if someone uses both. I'd prefer not to have to make a breaking release until we have some larger changes built up. |
Currently, we don't change the previous way. The previous way is basically how it is handled internally anyway, always felt a bit hacky and incomplete. The new version uses the interface of the previous way. |
OK, but if someone passes it the previous way via a kwarg it should now give a deprecation warning but still work, unless they pass it both the new way and the old way, in which case an error should be printed. That way we aren't making a breaking change, but we are warning users the old interface will go away in the future. |
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.
We already have lots of solving tests, which are pretty expensive on CI time, and I thought we already had noise scaling tests? So here you can just test if the generated problem gets the right parameters in it, and avoid more solving tests.
src/reactionsystem.jl
Outdated
@@ -1495,7 +1520,7 @@ end | |||
# SDEProblem from AbstractReactionNetwork | |||
function DiffEqBase.SDEProblem(rs::ReactionSystem, u0, tspan, | |||
p = DiffEqBase.NullParameters(), args...; | |||
noise_scaling = nothing, name = nameof(rs), | |||
noise_scaling = get_noise_scaling(rs), name = nameof(rs), |
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'd suggest we deprecate this, so by default it is nothing
. If a user passes anything here and get_noise_scaling
is empty we can use it, but give a warning that it is deprecated. Likewise, if a user already set get_noise_scaling
and uses this we give an error that both can't be used. Then in a future breaking release we can drop this kwarg, which is non-standard for SDEProblem
anyway.
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.
Sounds good, yeah, I will do this.
New update:
|
src/reactionsystem.jl
Outdated
@@ -1500,6 +1526,10 @@ function DiffEqBase.SDEProblem(rs::ReactionSystem, u0, tspan, | |||
include_zero_odes = true, checks = false, | |||
check_length = false, | |||
remove_conserved = false, kwargs...) | |||
if !isnothing(noise_scaling) | |||
any(isnoisescalingparameter.(parameters(rs))) && error("You have declared some paraemters as noise scaling parameters, and also given a \"noise_scaling\" argument to SDEProblem. Please remove the \"noise_scaling\", as this way of scaling CLE noise is being depricated.") |
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.
This check should be in convert
I think, since people may call that manually.
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.
In convert it if you add it after flattening on flatrs
you can just do
any(isnoisescalingparameter, get_ps(flatrs))
to avoid any allocations.
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 was thinking that this wasn't possible for some reason, but you are totally right, this should be moved to covert
.
I'm sorry, but I am stuck. I've been trying all evening, but have not been able to reuse the species approach, having tried several versions on it. I think I am almost there, although it required a new Here is my macro: macro noise_scaling_parameters(ex...)
vars = Symbolics._parse_parameters(:parameters, Real, ex)
# vector of symbols that get defined
lastarg = vars.args[end]
# start adding metadata statements where the vector of symbols was previously declared
idx = length(vars.args)
resize!(vars.args, idx + length(lastarg.args) + 1)
for sym in lastarg.args
vars.args[idx] = :($sym = ModelingToolkit.wrap(setmetadata(ModelingToolkit.value($sym),
Catalyst.NoiseScalingParameter,
true)))
idx += 1
end
# check nothing was declared isconstantspecies
ex = quote end
vars.args[idx] = ex
idx += 1
# put back the vector of the new species symbols
vars.args[idx] = lastarg
esc(vars)
end however using Catalyst
@parameters k1 k2
@variables t
@species X1(t) X2(t)
reaction = Reaction(k1, [X1], [X2], [1], [1])
@named rs = ReactionSystem([reaction], t, [X1, X2], [k1, k2, η]) gives I try putting a
and
but I cannot interpret this very well. |
We should make it a normal standalone macro too though. Maybe get that working first? |
I can take a closer look later today. |
@TorkelE I think the issue is you aren't calling |
src/reactionsystem.jl
Outdated
if !isnothing(noise_scaling) | ||
!isnothing(get_noise_scaling(rs)) && error("You have declared some parameters as noise scaling parameters, and also given a \"noise_scaling\" argument to SDEProblem. Please remove the \"noise_scaling\", as this way of scaling CLE noise is being depricated.") | ||
@warn "Passing noise scaling input into SDEProblem will be deprecated. New standard is to declare one (or several) paraemter as noise scaling parameters when the ReactionSystem is created. Please read https://docs.sciml.ai/Catalyst/stable/catalyst_applications/advanced_simulations/#Scaling-the-noise-magnitude-in-the-chemical-Langevin-equations." | ||
end |
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.
Shouldn't this check be in the convert
call?
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.
- Tests for standalone
@noise_scaling_parameters
macro? - Test that interpolation works with noise scaling? Both inside and outside the DSL? i.e.
a = :b
@noise_scaling_parameters $(a)
# check that the symbolic b works / becomes available?
Wil lfix this |
Your comments should be fixed. I have also updated to check that Regarding interpolation to make this work: η_stored = :η
rn = @reaction_network begin
@noise_scaling_parameters :(η_stored)
(p, d), 0 <--> X
end this is currently not possible due to #692. I also wanted to use interpolation for the η_stored = :η
@variables t
@species X1(t) X2(t)
@noise_scaling_parameters $(η_stored)=0.0
@parameters k1 k2
r1 = Reaction(k1,[X1],[X2],[1],[1])
r2 = Reaction(k2,[X2],[X1],[1],[1])
@named noise_scaling_network = ReactionSystem([r1, r2], t, [X1, X2], [k1, k2, η]) since simply running I could add tests for these two cases and mark them as broken, but until we figure #692 out I think that is as far as we can get. |
The second (non-DSL) example should work -- it works with |
If it is desired behaviour I will do the second one with η_stored = :η
@variables t
@species X1(t) X2(t)
ηs = @noise_scaling_parameters $(η_stored)=0.0
@parameters k1 k2
r1 = Reaction(k1,[X1],[X2],[1],[1])
r2 = Reaction(k2,[X2],[X1],[1],[1])
@named noise_scaling_network = ReactionSystem([r1, r2], t, [X1, X2], [k1, k2, ηs[1]]) |
Since we are more or less set on syntax, I have rewritten the noise scaling documentation given this update. I have also:
For the second case, I tried to add API for |
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.
It really feels like maybe we should consider an interface here in which a Reaction
takes a noise scaling parameter as a field. Then one could control the scaling on a per-reaction basis. Or at least somehow we attach a mapping from Reaction
to noise scaling parameter somewhere in the system.
If we do add it to reactions we'll have to make sure stuff like
Catalyst.jl/src/reactionsystem.jl
Line 231 in 046ce75
function ModelingToolkit.namespace_equation(rx::Reaction, name; kw...) |
gets updated appropriately.
8ba7a2e
to
141bb6e
Compare
87a576c
to
7dbb02c
Compare
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.
@TorkelE lots of typos, misspellings in the docs here. Please give a careful reread -- I stopped flagging them after a point.
In the docs it would probably be good to show the actual scaled mathematical equations of at least one example to make completely clear how the scaling comes into play.
Finally, I don't like this mixing of noise scaling being metadata at both system and reaction levels and the changes it then induces in the DSL code (making it more complicated for such a niche case). It seems like if one wants to apply a uniform scaling to all reactions and not specify it when creating reactions, this would be better to be implemented as a system transformation function in the API as I mention in my comments.
src/reaction_network.jl
Outdated
@@ -651,7 +658,8 @@ function push_reactions!(reactions::Vector{ReactionStruct}, sub_line::ExprValues | |||
for i in 1:maximum(lengs) | |||
# If the `only_use_rate` metadata was not provided, this has to be infered from the arrow used. | |||
metadata_i = get_tup_arg(metadata, i) | |||
if all(arg.args[1] != :only_use_rate for arg in metadata_i.args) | |||
merge_metadata!(metadata_i, default_reaction_metadata) |
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.
Having metadata for the same things at both the system and individual reaction level seems quite likely to lead to confusing behavior and bugs. Why have both? If one wants to give metadata for a reaction one should specify it at the reaction level. If one wants to apply metadata uniformly to all reactions, i.e. like a uniform noise scaling, that seems better implemented as a system transformation that just regenerates all the reactions with the desired scaling inplace. i.e. scaledsys = scale_reaction_noise_by(scaling, sys)
or such, with this function adding the scaling into each reaction individually.
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.
We can just add such a function to the API, and it will keep the DSL code and internal code simpler since we don't have to worry about having mutually interacting metadata in multiple places...
u0 = [:X1 => 1000.0, :X2 => 1000.0, :X3 => 1000.0] | ||
ps = [noise_scaling_network.p => 1000.0, noise_scaling_network.d => 1.0, noise_scaling_network.η1 => 1.0] | ||
sol = solve(SDEProblem(noise_scaling_network, u0, (0.0, 1000.0), ps), ImplicitEM(); saveat=1.0) | ||
@test var(sol[:X1]) < var(sol[:X2]) | ||
@test var(sol[:X1]) < var(sol[:X3]) |
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.
Would it not be better to just write a function to extract the noise coefficient from each equation in the generated SDESystem
and then check it is correct for each term? That seems more robust than the sol
test here (which I don't really see much value to, and seems like it could randomly fail on us).
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.
This has the same issue with the test potentially failing doesn't it?
u0 = [:X1 => 1000.0, :X2 => 1000.0, :X3 => 1000.0, :X4 => 1000.0, :X5 => 1000.0, :N1 => 3.0, :N3 => 0.33] | ||
ps = [:p => 1000.0, :d => 1.0, :η1 => 1.0, :η2 => 1.4, :η3 => 0.33, :η4 => 4.0] | ||
sol = solve(SDEProblem(noise_scaling_network, u0, (0.0, 1000.0), ps), ImplicitEM(); saveat=1.0, adaptive=false, dt=0.1) | ||
@test var(sol[:X1]) > var(sol[:X2]) > var(sol[:X3]) > var(sol[:X4]) > var(sol[:X5]) |
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.
Similar comments to above.
Agreed here, the interpretation is not necessarily unique like this. |
Co-authored-by: Sam Isaacson <[email protected]>
Co-authored-by: Sam Isaacson <[email protected]>
Co-authored-by: Sam Isaacson <[email protected]>
Just want to point out that we never mix system and reaction level metadata, that is just a misunderstanding. The only case where we (currently) use reaction metadata is one where you (in almost all cases) want the same value across all reactions. Hence, I added an option
which just sets that input (here (This is something I've used personally to avoid writing the same noise scaling for 30 reactions in a stress response model) |
See above responses. I will have another rearead through the doc thing. Generally, I didn't spend as much time on it as I should, because I had already had to re-write it once unnecessarily, and this is a part that I plan to rewrite once we redo parts of the docs (and I didn't want to spend too much effort on it until that is done). |
But why not add a default via a system transformation instead of a special DSL command. The former would have the benefit of preserving equivalence between DSL and symbolic construction, and work for both. The system transformation could be something like |
Even if we keep the DSL command, I'd advocate that it simply correspond to applying that transformation function after creating the system within the DSL instead of merging scalings via processing of the DSL expressions. It seems cleaner to me, and ensures consistency between DSL and symbolic approaches (plus it seems like a useful function to have in general). |
I just don't see why this specific thing should be a system transformation, and not:
and so on. Right now there is no part of model creation that is handled as a system transformation (if we ignore composition) rather than just being something you define at system creation. There might be some good reason why just noise scaling in SDE should be the exception, but I think in that case we'd have to discuss that in person. We can add a default argument for noise scaling in the |
Just to address some of these points:
We don't use system metadata? (I know MTK has a notion of it, but I don't think it is used by anything except JuliaHub internal stuff?)
This was an MTK choice long ago, so not really something we control. I agree it is confusing to have three ways to specify the default value of a parameter or initial condition (i.e. via metadata to the symbolic, via defaults, or via the mappings). But that just supports the idea that we should only handle noise scaling in one way (i.e. as metadata within reactions), and not try to also handle it with, for example, a conflicting system property that complicates things more.
This is a new choice made by MTK, and not something we control in Catalyst, but I don't see how it relates to this PR at all. I personally would have preferred if this decision was only made at problem construction time, as it reduces flexibility in modeling as you've observed in some of the broken tests I think. But it is done so we need to deal with it. Here is why I prefer making this a transformation:
Anyways, we can discuss more on Wednesday if needed. |
I agree in principle, but think this is a combination of refactoring and new features, and something that can be saved for a later day. Yes, discussing it on Wednesday is probably better to ensure that we are on the same page. |
src/reactionsystem.jl
Outdated
set_default_metadata(eq::Equation, default_metadata) = eq | ||
|
||
""" | ||
remake_noise_scaling(rs::ReactionSystem, noise_scaling) |
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.
remake_noise_scaling(rs::ReactionSystem, noise_scaling) | |
scale_noise(rs::ReactionSystem, noise_scaling) |
I think this is a more concise name, and don't really see the need for remake
here.
You might, however, consider a keyword preserve_existing
that allows users to control if existing values are overwritten?
Co-authored-by: Sam Isaacson <[email protected]>
end | ||
|
||
# For a `ReactionSystem`, updates all `Reaction`'s default metadata. | ||
function set_default_metadata(rs::ReactionSystem; default_reaction_metadata = []) |
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.
Does the scaling just have to be a parameter or can it be a more general expression?
Somewhere we probably need a check that the passed in metadata only involves parameters/species/variables that are already in our system, or we also need to add those to the new system.
Co-authored-by: Sam Isaacson <[email protected]>
u0 = [:X1 => 1000.0, :X2 => 3000.0] | ||
sol_1_1 = solve(SDEProblem(noise_scaling_network_1, u0, (0.0, 1000.0), [:k1 => 2.0, :k2 => 0.66, :η1 => 2.0, :η2 => 2.0]), ImplicitEM(); saveat=1.0) | ||
sol_1_2 = solve(SDEProblem(noise_scaling_network_1, u0, (0.0, 1000.0), [:k1 => 2.0, :k2 => 0.66, :η1 => 2.0, :η2 => 0.2]), ImplicitEM(); saveat=1.0) | ||
sol_1_3 = solve(SDEProblem(noise_scaling_network_1, u0, (0.0, 1000.0), [:k1 => 2.0, :k2 => 0.66, :η1 => 0.2, :η2 => 0.2]), ImplicitEM(); saveat=1.0) | ||
@test var(sol_1_1[1,:]) > var(sol_1_2[1,:]) > var(sol_1_3[1,:]) | ||
|
||
noise_scaling_network_2 = @reaction_network begin | ||
@parameters η[1:2] | ||
(k1, k2), X1 ↔ X2, ([noise_scaling=η[1]],[noise_scaling=η[2]]) | ||
end | ||
@unpack k1, k2, η = noise_scaling_network_2 | ||
sol_2_1 = solve(SDEProblem(noise_scaling_network_2, u0, (0.0, 1000.0), [k1 => 2.0, k2 => 0.66, η[1] => 2.0, η[2] => 2.0]), ImplicitEM(); saveat=1.0) | ||
sol_2_2 = solve(SDEProblem(noise_scaling_network_2, u0, (0.0, 1000.0), [k1 => 2.0, k2 => 0.66, η[1] => 2.0, η[2] => 0.2]), ImplicitEM(); saveat=1.0) | ||
sol_2_3 = solve(SDEProblem(noise_scaling_network_2, u0, (0.0, 1000.0), [k1 => 2.0, k2 => 0.66, η[1] => 0.2, η[2] => 0.2]), ImplicitEM(); saveat=1.0) | ||
@test var(sol_2_1[1,:]) > var(sol_2_2[1,:]) > var(sol_2_3[1,:]) | ||
end |
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 thought I commented on this before. Aren't these tests not guaranteed to always pass? The variance could happen for a given random number stream to not satisfy these inequalities right? These need to be deterministic tests that will always succeed (for example, by using the same rng stream each time).
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.
More generally though I don't understand this test. It isn't testing accuracy in any real quantitative way, so what is the point? I'd suggest picking a statistic of a specific system with a specific scaling and checking if you recover that statistic to a given accuracy with enough samples.
One of the more niche features is the ability to (linearly) scale the noise magnitude in the CEL: https://docs.sciml.ai/Catalyst/stable/catalyst_applications/advanced_simulations/#Scaling-the-noise-magnitude-in-the-chemical-Langevin-equations. The current interface was a bit "clanky".
New new interface (as opposed to the new one from last year that was meant to override the old one).
Reactions can be given an
noise_scaling
metadata. This is any expression, which is multiplied by the noise terms generated by that reaction in the CLE. Any normal expression is valid. E.gIf you want to have a parameter as a noise scaling parameter (like previously), you have to create this separately:
For cases where you want the same noise scaling in all reaction, I have added a
@default_noise_scaling
option:You can still supply
noise_scaling
metadata to individual reactions when using@default_noise_scaling
(I'm which case the default value is overridden).I considered to have a option
@default_noise_scaling_parameter η
that combinesbut decided to keep it like this.
Since we are having a breaking release, and probably the last one in a while, I am deprecating the previous syntax fully (only an error message left).