Skip to content
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

CoevolveSynced #315

Merged
merged 57 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e1b62f2
implements thinning synced with stepper.
gzagatti Apr 15, 2023
f379bd8
fix re-use of rng when stepping through thinning.
gzagatti Apr 17, 2023
7c342f3
avoids saving during each step of the thinning algorithm and saves on…
gzagatti Apr 17, 2023
b1fcab4
do not replace Coevolve; add CoevolveSynced.
gzagatti Apr 17, 2023
c44a539
adds CoevolveSynced to SIR tutorial.
gzagatti Apr 18, 2023
d5fbdc3
linting.
gzagatti Apr 18, 2023
9743df3
adds assertion check.
gzagatti Apr 19, 2023
741b2c3
adds warning messages.
gzagatti Apr 21, 2023
e88cf7b
passes integrator.u to update_dependent_rates! instead.
gzagatti Apr 21, 2023
6b89faf
fix bimol test
gzagatti Apr 21, 2023
ce7e68d
Merge remote-tracking branch 'origin/master' into coevolve-ii
gzagatti Apr 21, 2023
60d4e27
fix merge with master/origin
gzagatti Apr 21, 2023
6bf486b
fix merge with master/origin
gzagatti Apr 22, 2023
6b94480
passes integrator.p instead
gzagatti Apr 22, 2023
a3ed1d5
removes file added by mistake
gzagatti Apr 22, 2023
ea77e42
a better way of rejecting jumps
gzagatti Apr 26, 2023
d532727
fix save_positions when dealing with discrete or continuous steppers
gzagatti Apr 26, 2023
a76b671
adds test for save_positions
gzagatti Apr 26, 2023
446444e
order callbacks consistently
gzagatti Apr 27, 2023
d303fb3
adds dependency to documentation
gzagatti May 3, 2023
661b572
moves CoevolveSynced to Coevolve, removes the original Coevolve
gzagatti May 17, 2023
84eec82
Merge remote-tracking branch 'origin/master' into coevolve-ii
gzagatti May 17, 2023
6223d89
modifies the documentation given changes to Coevolve
gzagatti May 17, 2023
35e8bb4
fixes test and formatting
gzagatti May 17, 2023
8686fca
updates test/save_positions.jl
gzagatti Jul 11, 2023
91a001f
updates src/aggregators/coevolve.jl
gzagatti Jul 11, 2023
0e1bf77
updates src/aggregators/coevolve.jl
gzagatti Jul 11, 2023
c27fdbd
updates src/aggregators/coevolve.jl
gzagatti Jul 11, 2023
ef610bf
updates test/save_positions.jl
gzagatti Jul 11, 2023
fd4d428
updates docs/src/jump_types.md
gzagatti Jul 11, 2023
667b362
updates src/aggregators/coevolve.jl, avoids type instability.
gzagatti Jul 11, 2023
88b58e8
update test/save_positions.jl
gzagatti Jul 12, 2023
9f2e0de
removes unnecessary Graph dependency
gzagatti Jul 12, 2023
0dcebe1
revert to let block
gzagatti Jul 12, 2023
0732f7c
updates doc entry
gzagatti Jul 12, 2023
1a810ea
updates doc entry
gzagatti Jul 12, 2023
52b4da1
fixes lonely rng
gzagatti Jul 12, 2023
8d75863
remove safety checks for consistency and efficiency reasons
gzagatti Jul 12, 2023
73b947c
fix reinit of exisiting problem with FunctionWrappers
isaacsas Jul 10, 2023
2d3dbc6
add tests
isaacsas Jul 10, 2023
74437c5
updates
isaacsas Jul 10, 2023
e36e8a1
fix spatial test
isaacsas Jul 10, 2023
0ad2e3f
Update Project.toml
isaacsas Jul 10, 2023
361fe13
fix failing test
isaacsas Jul 11, 2023
69ff6fd
simpler fix
isaacsas Jul 11, 2023
b2d591b
fix dependency issues
gzagatti Jul 12, 2023
86f2143
Merge remote-tracking branch 'origin/master' into coevolve-ii
gzagatti Jul 12, 2023
4e0dcb5
update docs/src/tutorials/discrete_stochastic_example.md
gzagatti Jul 14, 2023
ba5aaf3
update docs/src/tutorials/discrete_stochastic_example.md
gzagatti Jul 14, 2023
076104c
adds comments
gzagatti Jul 14, 2023
a8ec135
adds comment
gzagatti Jul 14, 2023
b4b0e01
test rate, urate and lrate not called past tstop (issue #330)
gzagatti Jul 14, 2023
bc4d763
avoid skipping callbacks when candidate rejected.
gzagatti Jul 15, 2023
fcd48cc
only saves when a jump occurs
gzagatti Jul 15, 2023
ce725ff
additional documentation on SSAStepper saving behaviour
gzagatti Jul 15, 2023
e87f020
tweak SSAStepper saving description
isaacsas Jul 16, 2023
b3f88ab
format
isaacsas Jul 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 45 additions & 32 deletions docs/src/jump_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,24 +148,24 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs = not
`k*A*(A-1)*(A-2)/3!`. To *avoid* having the reaction rates rescaled (by `1/2`
and `1/6` for these two examples), one can pass the `MassActionJump`
constructor the optional named parameter `scale_rates = false`, i.e., use

```julia
MassActionJump(reactant_stoich, net_stoich; scale_rates = false, param_idxs)
```

- Zero order reactions can be passed as `reactant_stoich`s in one of two ways.
Consider the ``\varnothing \overset{k}{\rightarrow} A`` reaction with rate
`k=1`:

```julia
p = [1.0]
reactant_stoich = [[0 => 1]]
net_stoich = [[1 => 1]]
jump = MassActionJump(reactant_stoich, net_stoich; param_idxs = [1])
```

Alternatively, one can create an empty vector of pairs to represent the reaction:

```julia
p = [1.0]
reactant_stoich = [Vector{Pair{Int, Int}}()]
Expand All @@ -174,13 +174,13 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs = not
```
- For performance reasons, it is recommended to order species indices in
stoichiometry vectors from smallest to largest. That is

```julia
reactant_stoich = [[1 => 2, 3 => 1, 4 => 2], [2 => 2, 3 => 2]]
```

is preferred over

```julia
reactant_stoich = [[3 => 1, 1 => 2, 4 => 2], [3 => 2, 2 => 2]]
```
Expand Down Expand Up @@ -212,15 +212,6 @@ must also specify
- `rateinterval(u, p, t)`, a function which computes a time interval `t` to `t + rateinterval(u, p, t)` given state `u` and parameters `p` over which the
`urate` bound will hold (and `lrate` bound if provided, see below).

Note that it is ok if the `urate` bound would be violated within the
`rateinterval` due to a change in `u` arising from another `ConstantRateJump`,
`MassActionJump` or *bounded* `VariableRateJump` being executed, as the chosen
aggregator will then handle recalculating the rate bound and interval. *However,
if the bound could be violated within the time interval due to a change in `u`
arising from continuous dynamics such as a coupled ODE, SDE, or a general
`VariableRateJump`, bounds should not be given.* This ensures the jump is
classified as a general `VariableRateJump` and properly handled.

For increased performance, one can also specify a lower bound that should be
valid over the same `rateinterval`

Expand All @@ -232,14 +223,32 @@ valid over the same `rateinterval`
Note that

- It is currently only possible to simulate `VariableRateJump`s with
`SSAStepper` when using systems with only bounded `VariableRateJump`s and the
`SSAStepper` when using systems with bounded `VariableRateJump`s and the
`Coevolve` aggregator.
- When choosing a different aggregator than `Coevolve`, `SSAStepper` cannot
currently be used, and the `JumpProblem` must be coupled to a continuous
problem type such as an `ODEProblem` to handle time-stepping. The continuous
time-stepper treats *all* `VariableRateJump`s as `ContinuousCallback`s, using
the `rate(u, p, t)` function to construct the `condition` function that
triggers a callback.
- Any `JumpProblem` with `VariableRateJump` that *does not use* the
`Coevolve` aggregator must be coupled to a continuous problem type such as
an `ODEProblem` to handle time-stepping. Continuous time-stepper will ignore
the provided aggregator and treat *all* `VariableRateJump`s as
`ContinuousCallback`s, using the `rate(u, p, t)` function to construct the
`condition` function that triggers a callback.
- When using `Coevolve` with a `JumpProblem` coupled to a continuous problem
such as an `ODEProblem`, the aggregator will handle the jumps in same way
that it does with `SSAStepper`. However, *ensure that given `t` the bounds
will hold for the duration of `rateinterval(t)` for the full coupled system's
dynamics or the algorithm will not give correct samples*. Numerical and
analytical solutions are generally not guaranteed to satisfy the same bounds,
especially in large complicated models. Consider adding some slack on the
bounds and approach complex models with care. In most simple cases the bounds
should be close enough. For debugging purposes one might want to add safety
checks in the bound functions.
- In some circumstances with complex model of many variables it can be
difficult to determine good a priori bounds on the ODE variables. For some
discussion on the bound setting problem see [1].

[1] V. Lemaire, M. Thieullen and N. Thomas, Exact Simulation of the Jump
Times of a Class of Piecewise Deterministic Markov Processes, Journal of
Scientific Computing, 75 (3), 1776-1807 (2018).
doi:10.1007/s10915-017-0607-4.

#### Defining a Regular Jump

Expand Down Expand Up @@ -331,10 +340,12 @@ aggregator requires various types of dependency graphs, see the next section):
- *`NRM`*: The Gibson-Bruck Next Reaction Method [7]. For some reaction network
structures, this may offer better performance than `Direct` (for example,
large, linear chains of reactions).
- *`Coevolve`*: An adaptation of the COEVOLVE algorithm of Farajtabar et al [8].
Currently the only aggregator that also supports *bounded*
`VariableRateJump`s. Essentially reduces to `NRM` in handling
`ConstantRateJump`s and `MassActionJump`s.
- *`Coevolve`*: An improvement of the COEVOLVE algorithm of Farajtabar et al
[8]. Currently the only aggregator that also supports *bounded*
`VariableRateJump`s. As opposed
to COEVOLVE, this method syncs the thinning procedure with the stepper
which allows it to handle dependencies on continuous dynamics. Essentially
reduces to `NRM` in handling `ConstantRateJump`s and `MassActionJump`s.

To pass the aggregator, pass the instantiation of the type. For example:

Expand All @@ -345,7 +356,7 @@ JumpProblem(prob, Direct(), jump1, jump2)
will build a problem where the jumps are simulated using Gillespie's Direct SSA
method.

[1] Daniel T. Gillespie, A general method for numerically simulating the stochastic
[1] D. T. Gillespie, A general method for numerically simulating the stochastic
time evolution of coupled chemical reactions, Journal of Computational Physics,
22 (4), 403–434 (1976). doi:10.1016/0021-9991(76)90041-3.

Expand Down Expand Up @@ -431,10 +442,12 @@ For representing and aggregating jumps
jumps.
- Use `VariableRateJump`s for any remaining jumps with variable rate between
jumps. If possible, construct a bounded [`VariableRateJump`](@ref) as
described above and in the doc string. The tighter and easier to compute the
bounds are, the faster the resulting simulation will be. Use the `Coevolve`
aggregator to ensure such jumps are handled via the more efficient aggregator
interface.
described above and in the doc string. The tighter and easier to compute
the bounds are, the faster the resulting simulation will be. Use the
`Coevolve` aggregator to ensure such jumps are handled via the more
efficient aggregator interface. `Coevolve` handles continuous steppers so
can be coupled with a continuous problem type such as an `ODEProblem` as
long as the bounds are satisfied given changes in `u` over `rateinterval`.

For systems with only `ConstantRateJump`s and `MassActionJump`s,

Expand Down
111 changes: 90 additions & 21 deletions docs/src/tutorials/discrete_stochastic_example.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,10 +596,10 @@ jump4 = ConstantRateJump(rate4, affect4!)
With the jumps defined, we can build a
[`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_types/).
Bounded `VariableRateJump`s over a `DiscreteProblem` can currently only be
simulated with the `Coevolve` aggregator. The aggregator requires a dependency
graph to indicate when a given jump occurs which other jumps in the system
should have their rate recalculated (i.e., their rate depends on states modified
by one occurrence of the first jump). This ensures that rates, rate bounds, and
simulated with `Coevolve`. The aggregator requires a dependency graph to
indicate when a given jump occurs and which other jumps in the system should
have their rate recalculated (i.e., their rate depends on states modified by
one occurrence of the first jump). This ensures that rates, rate bounds, and
rate intervals are recalculated when invalidated due to changes in `u`. For the
current example, both processes mutually affect each other, so we have

Expand Down Expand Up @@ -628,11 +628,13 @@ We see that the time-dependent infection rate leads to a lower peak of the
infection throughout the population.

Note that bounded `VariableRateJump`s over `DiscreteProblem`s can be quite
general, but it is not possible to handle rates that change according to an
ODE/SDE modified variable. A rate such as `p[2]*u[1]*u[4]` when `u[4]` is the
solution of a continuous problem such as an ODE or SDE can only be handled using
a general `VariableRateJump` within a continuous integrator as discussed
[below](@ref VariableRateJumpSect).
general. However, when handling rates that change according to an ODE/SDE
modified variable we will need a continuous integrator. One example is
discussed [below](@ref VariableRateJumpSect) in which we have a new reaction
added to the model with rate `p[2]*u[1]*u[4]` where `u[4]` is the solution of
an ODE. In such models, you will also need to be more careful in setting
rate bounds as they must be valid for the full coupled system's
dynamics.

## [Reducing Memory Use: Controlling Saving Behavior](@id save_positions_docs)

Expand Down Expand Up @@ -701,7 +703,7 @@ plot(sol; label = ["S(t)" "I(t)" "R(t)"])
```

Note that we can combine `MassActionJump`s, `ConstantRateJump`s and bounded
`VariableRateJump`s using the `Coevolve` aggregator.
`VariableRateJump`s using the `Coevolve` aggregators.

## Adding Jumps to a Differential Equation

Expand All @@ -713,7 +715,7 @@ only acts on some new 4th component:
```@example tut2
using OrdinaryDiffEq
function f(du, u, p, t)
du[4] = u[2] * u[3] / 100000 - u[1] * u[4] / 100000
du[4] = u[2] * u[3] / 1e5 - u[1] * u[4] / 1e5
nothing
end
u₀ = [999.0, 10.0, 0.0, 100.0]
Expand All @@ -733,13 +735,14 @@ sol = solve(jump_prob, Tsit5())
plot(sol; label = ["S(t)" "I(t)" "R(t)" "u₄(t)"])
```

Note, when using `ConstantRateJump`s, `MassActionJump`s, and bounded
`VariableRateJump`s, the ODE derivative function `f(du, u, p, t)` should not
modify any states in `du` that the corresponding jump rate functions depend on.
However, the opposite where jumps modify the ODE variables is allowed. If one
needs to change a component of `u` in the ODE for which a rate function is
dependent, then one must use a general `VariableRateJump` as described in the
next section.
Note that in general, the ODE derivative `f(du, u, p, t)` could modify any
element in `du` which the jump rate functions depend on. In this section, `f(du,
u, p, t)` does not modify the jump rates so it is safe to couple them with any
type of jump and use any type of aggregator. However, the implementation does
not enforce this requirement, so one must be careful. Alternatively, when
`f(du, u, p, t)` *does* modify variables that affect the jump rate, we have to
implement another strategy as described in the next [next Section](@ref
VariableRateJumpSect].

## [Adding a general VariableRateJump that Depends on a Continuously Evolving Variable](@id VariableRateJumpSect)

Expand All @@ -758,10 +761,10 @@ jump5 = VariableRateJump(rate5, affect5!)
```

Notice, since `rate5` depends on a variable that evolves continuously, and hence
is not constant between jumps, *we must use a general `VariableRateJump` without
upper/lower bounds*.
is not constant between jumps, *we must either use a general `VariableRateJump` without
upper/lower bounds or a bounded `VariableRateJump`*.

Solving the equation is exactly the same:
In the general case, solving the equation is exactly the same:

```@example tut2
u₀ = [999.0, 10.0, 0.0, 1.0]
Expand All @@ -774,6 +777,72 @@ plot(sol; label = ["S(t)" "I(t)" "R(t)" "u₄(t)"])
*Note that general `VariableRateJump`s require using a continuous problem, like
an ODE/SDE/DDE/DAE problem, and using floating point initial conditions.*

Alternatively, the case of bounded `VariableRateJump` requires some maths.
First, we need to obtain the upper bounds of `rate5` at time `t` given `u`.
Note that `rate5` evolves according to `u[4]` which is a separable first order
differential equation of the form ``x' = b - a x`` with general solution:

```math
x(t) = - \frac{e^{-a t - c_1 a}}{a} + \frac{b}{a}
```

This is bounded by ``b / a`` which is too high for our purposes since it would
lead to a high rate of rejection during sampling. However, since the function
is increasing we can compute the upper bound given an interval ``\Delta t``
as following:

```math
\bar{x}(s) = x(t) \, e^{-a (t + \Delta t)} + \frac{b}{a} (1 - e^{- a (t + \Delta t)}) \text{ , } \forall s \in [t, t + \Delta t]
gzagatti marked this conversation as resolved.
Show resolved Hide resolved
```

However, when ``a = 0`` the differential equation becomes ``x' = b`` whose solution is ``x(t) = b t``. In which case, we obtain a different upper bound given by:

```math
\bar{x}(s) = x(t) + b * (t + \Delta t) \text{ , } \forall s \in [t, t + \Delta t]
```

These expressions allow us to write the upper-bound and the rate interval in
Julia. In this example we use analytical boundaries for *illustration
purposes*. However, in some circumstances with complex model of many variables
it can be difficult to determine good a priori bounds on the ODE variables.
Moreover, numerical and analytical solutions are generally not guaranteed to
strictly satisfy the same bounds. In most cases the bounds should be close
enough. Thus, consider adding some slack on the bounds and approach complex
models with care.

```@example tut2
function urate2(u, p, t)
if u[1] > 0
1e-2 * max(u[4],
(u[4] * exp(-1 * u[1] / 1e5) +
(u[2] * u[3] / u[1]) * (1 - exp(-1 * u[1] / 1e5))))
else
1e-2 * (u[4] + 1 * u[2] * u[3] / 1e5)
end
end
rateinterval2(u, p, t) = 1
```

We can then formulate the jump problem. The only aggregator that supports
bounded `VariableRateJump`s is `Coevolve`. We formulate and solve the
jump problem with this aggregator. `Coevolve` can be formulated as either
a discrete or continuous problem. In this case, we must formulate the problem
as continuous as it depends on a continuous variable.

```@example tut2
jump6 = VariableRateJump(rate5, affect5!; urate = urate2, rateinterval = rateinterval2)
dep_graph2 = [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
jump_prob = JumpProblem(prob, Coevolve(), jump, jump2, jump6; dep_graph = dep_graph2)
sol = solve(jump_prob, Tsit5())
plot(sol; label = ["S(t)" "I(t)" "R(t)" "u₄(t)"])
```

We obtain the same solution as with `Direct`, but `Coevolve` runs faster
because it doesn't need to compute the derivative of `rate5`. Each aggregator
faces a different trade-off, so the the choice of best aggregator will depend
on the problem at hand. `Coevolve` requires a good understanding of the
equations involved, passing a wrong boundary can result in silent bugs.

Lastly, we are not restricted to ODEs. For example, we can solve the same jump
problem except with multiplicative noise on `u[4]` by using an `SDEProblem`
instead:
Expand Down
5 changes: 0 additions & 5 deletions docs/src/tutorials/jump_diffusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@ plot(sol)
In this way we have solved a mixed jump-ODE, i.e., a piecewise deterministic
Markov process.

Note that in this case, the rates of the `VariableRateJump`s depend on a
variable that is driven by an `ODEProblem`, and thus they would not satisfy the
conditions to be represented as bounded `VariableRateJump`s (and hence cannot
be simulated with the `Coevolve` aggregator).

## Jump Diffusion

Now we will finally solve the jump diffusion problem. The steps are the same
Expand Down
2 changes: 2 additions & 0 deletions src/JumpProcesses.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ abstract type AbstractJump end
abstract type AbstractMassActionJump <: AbstractJump end
abstract type AbstractAggregatorAlgorithm end
abstract type AbstractJumpAggregator end
abstract type AbstractSSAIntegrator{Alg, IIP, U, T} <:
DiffEqBase.DEIntegrator{Alg, IIP, U, T} end

import Base.Threads
@static if VERSION < v"1.3"
Expand Down
7 changes: 6 additions & 1 deletion src/SSA_stepper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Solution objects for pure jump problems solved via `SSAStepper`.
$(FIELDS)
"""
mutable struct SSAIntegrator{F, uType, tType, tdirType, P, S, CB, SA, OPT, TS} <:
DiffEqBase.DEIntegrator{SSAStepper, Nothing, uType, tType}
AbstractSSAIntegrator{SSAStepper, Nothing, uType, tType}
"""The underlying `prob.f` function. Not currently used."""
f::F
"""The current solution values."""
Expand Down Expand Up @@ -258,10 +258,15 @@ function DiffEqBase.step!(integrator::SSAIntegrator)
# FP error means the new time may equal the old if the next jump time is
# sufficiently small, hence we add this check to execute jumps until
# this is no longer true.
integrator.u_modified = true
while integrator.t == integrator.tstop
doaffect && integrator.cb.affect!(integrator)
end

if !integrator.u_modified
return nothing
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChrisRackauckas is this usage of u_modified reasonable? (And more generally, how it is used in this PR for pure jump and hybrid models?)

Also, this bails out of calling the discrete callbacks when a rejection-event is sampled, but I wonder if we might still want to allow user callbacks to get checked in this case?

gzagatti marked this conversation as resolved.
Show resolved Hide resolved
if !(typeof(integrator.opts.callback.discrete_callbacks) <: Tuple{})
discrete_modified, saved_in_cb = DiffEqBase.apply_discrete_callback!(integrator,
integrator.opts.callback.discrete_callbacks...)
Expand Down
10 changes: 6 additions & 4 deletions src/aggregators/aggregators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,12 @@ systems with many species and many channels, Journal of Physical Chemistry A,
struct NRM <: AbstractAggregatorAlgorithm end

"""
An adaptation of the COEVOLVE algorithm for simulating any compound jump process
that evolves through time. This method handles variable intensity rates with
user-defined bounds and inter-dependent processes. It reduces to NRM when rates
are constant.
An improvement of the COEVOLVE algorithm for simulating any compound jump
process that evolves through time. This method handles variable intensity
rates with user-defined bounds and inter-dependent processes. It reduces to
NRM when rates are constant. As opposed to COEVOLVE, this method syncs the
thinning procedure with the stepper which allows it to handle dependencies on
continuous dynamics.

M. Farajtabar, Y. Wang, M. Gomez-Rodriguez, S. Li, H. Zha, and L. Song,
COEVOLVE: a joint point process model for information diffusion and network
Expand Down
Loading
Loading