Skip to content

Commit 656c86a

Browse files
authored
Merge pull request #821 from SciML/structure_interfacing_doc
Structure interfacing doc
2 parents 0287fb4 + 3bb4c69 commit 656c86a

File tree

1 file changed

+154
-94
lines changed

1 file changed

+154
-94
lines changed
Lines changed: 154 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,201 @@
11
# [Interfacing problems, integrators, and solutions](@id simulation_structure_interfacing)
2-
When simulating a model, one begins with creating a [problem](https://docs.sciml.ai/DiffEqDocs/stable/basics/problem/). Next, a simulation is performed on a problem, during which the state of the simulation is recorded through an [integrator](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/). Finally, the simulation output is returned as a [solution](https://docs.sciml.ai/DiffEqDocs/stable/basics/solution/). This tutorial describes how to access, or modify the state, or parameter, values of problems, integrators, and solutions structures.
2+
When simulating a model, one begins with creating a [problem](https://docs.sciml.ai/DiffEqDocs/stable/basics/problem/). Next, a simulation is performed on the problem, during which the simulation's state is recorded through an [integrator](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/). Finally, the simulation output is returned as a [solution](https://docs.sciml.ai/DiffEqDocs/stable/basics/solution/). This tutorial describes how to access (or modify) the state (or parameter) values of problem, integrator, and solution structures.
33

4-
Generally, when we have a structure `simulation_struct` and want to interface with the unknown (or parameter) `G`, we use `simulation_struct[:G]` to access the value, and `simulation_struct[:G] = 5.0` to set it to a new value. However, see the following examples for full details.
4+
Generally, when we have a structure `simulation_struct` and want to interface with the unknown (or parameter) `x`, we use `simulation_struct[:x]` to access the value, and `simulation_struct[:x] = 5.0` to set it to a new value. For situations where a value is accessed (or changed) a large number of times, it can *improve performance* to first create a [specialised getter/setter function](@ref simulation_structure_interfacing_functions).
55

6-
!!! note
7-
The following tutorial will describe how to interface with problems, integrators, and solutions using `[]` notation. An alternative is to use the `ModelingToolkit.getu`, `ModelingToolkit.getp`, `ModelingToolkit.setu`, and `ModelingToolkit.setp` functions. These requires an additional step to use, however, they can also improve performance when a very large number interfaces are carried out.
8-
9-
## Interfacing problem objects
6+
## [Interfacing problem objects](@id simulation_structure_interfacing_problems)
107

11-
We begin by demonstrating how we can interface with problem objects. We will demonstrate using a `ODEProblem`, however, it works similarly for other problem types.
12-
```@example ex1
8+
We begin by demonstrating how we can interface with problem objects. First, we create an `ODEProblem` representation of a [chemical cross-coupling model](@ref ref) (where a catalyst, $C$, couples two substrates, $S₁$ and $S₂$, to form a product, $P$).
9+
```@example structure_indexing
1310
using Catalyst
14-
rn = @reaction_network begin
15-
(k1,k2), X1 <--> X2
11+
cc_system = @reaction_network begin
12+
k₁, S₁ + C --> S₁C
13+
k₂, S₁C + S₂ --> CP
14+
k₃, CP --> C + P
1615
end
1716
18-
u0 = [:X1 => 1.0, :X2 => 5.0]
19-
p = [:k1 => 5.0, :k2 => 2.0]
20-
oprob = ODEProblem(rn, u0, (0.0,10.0), p)
17+
u0 = [:S₁ => 1.0, :C => 0.05, :S₂ => 1.2, :S₁C => 0.0, :CP => 0.0, :P => 0.0]
18+
tspan = (0., 10.0)
19+
ps = [:k₁ => 5.0, :k₂ => 5.0, :k₃ => 100.0]
20+
oprob = ODEProblem(cc_system, u0, tspan, ps)
2121
nothing # hide
2222
```
2323

24-
We can find the value of a state simply by interfacing with the corresponding symbol:
25-
```@example ex1
26-
oprob[:X1]
24+
We can find a specie's (or [variable's](@ref ref)) initial condition value by simply indexing with the species of interest as input. Here we check the initial condition value of $C$:
25+
```@example structure_indexing
26+
oprob[:C]
2727
```
28+
An almost identical notation can be used for parameters, however, here we use `oprob.ps` (rather than `oprob`):
2829
with the notation being identical for parameters:
29-
```@example ex1
30-
oprob.ps[:k1]
30+
```@example structure_indexing
31+
oprob.ps[:k₁]
3132
```
32-
33-
If we want to change an unknown's initial condition value, we use the following notation
34-
```@example ex1
35-
oprob[:X1] = 10.0
33+
To retrieve several species initial condition (or parameter) values, simply give a vector input. Here we check the values of the two substrates ($S₁$ and $S₂$):
34+
```@example structure_indexing
35+
oprob[[:S₁, :S₂]]
3636
```
37-
with parameters using the same notation.
3837

39-
!!! note
40-
When interfacing with a parameter, `.ps` must be appended to the structure uses (e.g. `oprob`). This is not done when species are interfaced with.
41-
42-
#### [Remaking problems using the `remake` function](@id simulation_structure_interfacing_remake)
43-
Typically, when modifying problems, it is recommended to use the `remake` function. Unlike when we do `oprob[:X1] = 10.0` (which modifies the problem in question), `remake` creates a new problem object. The `remake` function takes a problem as input, and any fields you wish to modify (and their new values) as optional inputs. Thus, we can do:
44-
```@example ex1
45-
using DifferentialEquations
46-
@unpack X1, X2, k1, k2 = rn
47-
oprob1 = ODEProblem(rn, u0, (0.0,10.0), p)
48-
oprob2 = remake(oprob1; u0=[X1 => 10.0, X2 => 50.0], tspan=(0.0,100.0), p=[k1 => 50.0,k2 => 20.0])
49-
nothing # hide
50-
```
51-
and we can now check the fields of `oprob2`
52-
```@example ex1
53-
oprob2.u0
38+
We can change a species's initial condition value using a similar notation. Here we increase the initial concentration of $C$ (and also confirm that the new value is stored in an updated `oprob`):
39+
```@example structure_indexing
40+
oprob[:C] = 0.1
41+
oprob[:C]
5442
```
55-
```@example ex1
56-
oprob2.tspan
43+
Again, parameter values can be changed using a similar notation, however, again requiring `oprob.ps` notation:
44+
```@example structure_indexing
45+
oprob.ps[:k₁] = 10.0
46+
oprob.ps[:k₁]
5747
```
58-
```@example ex1
59-
oprob2.p
48+
Finally, vectors can be used to update multiple quantities simultaneously
49+
```@example structure_indexing
50+
oprob[[:S₁, :S₂]] = [0.5, 0.3]
51+
oprob[[:S₁, :S₂]]
6052
```
61-
Please note that, currently, `remake` does not work while giving `Symbol`s as input (e.g `[:X1 => 10.0, :X2 => 50.0]`), but we need to unpack the symbolic variables and use them instead (please see the end of this tutorial for more information on using symbolic variables rather than `Symbol`s).
53+
Generally, when updating problems, it is often better to use the [`remake` function](@ref simulation_structure_interfacing_problems_remake) (especially when several values are updated).
6254

63-
When using `remake`, we only have to provide the fields that we actually wish to change, e.g.
64-
```@example ex1
65-
oprob3 = remake(oprob1; u0=[X1 => 10.0, X2 => 50.0])
66-
nothing # hide
55+
!!! warn
56+
Indexing *should not* be used not modify `JumpProblem`s. Here, [remake](@ref ref) should be used exclusively.
57+
58+
A problem's time span can be accessed through the `tspan` field:
59+
```@example structure_indexing
60+
oprob.tspan
6761
```
68-
will only update the initial conditions.
6962

63+
!!! note
64+
Here we have used an `ODEProblem`to demonstrate all interfacing functionality. However, identical workflows work for the other problem types.
65+
66+
### [Remaking problems using the `remake` function](@id simulation_structure_interfacing_problems_remake)
67+
The `remake` function offers an (to indexing) alternative approach for updating problems. Unlike indexing, `remake` creates a new problem (rather than updating the old one). Furthermore, it permits the updating of several values simultaneously. The `remake` function takes the following inputs:
68+
- The problem that is remakes.
69+
- (optionally) `u0`: A vector with initial conditions that should be updated. The vector takes the same form as normal initial condition vectors, but does not need to be complete (in which case only a subset of the initial conditions are updated).
70+
- (optionally) `tspan`: An updated time span (using the same format as time spans normally are given in).
71+
- (optionally) `p`: A vector with parameters that should be updated. The vector takes the same form as normal parameter vectors, but does not need to be complete (in which case only a subset of the parameters are updated).
72+
73+
Here we modify our problem to increase the initial condition concentrations of the two substrates ($S₁$ and $S₂$), and also confirm that the new problem is different from the old (unchanged) one:
74+
```@example structure_indexing
75+
using OrdinaryDiffEq
76+
oprob_new = remake(oprob; u0 = [:S₁ => 5.0, :S₂ => 2.5])
77+
oprob_new == oprob
78+
```
79+
Here, we instead use `remake` to simultaneously update a all three fields:
80+
```@example structure_indexing
81+
oprob_new_2 = remake(oprob; u0 = [:C => 0.2], tspan = (0.0, 20.0), p = [:k₁ => 2.0, :k₂ => 2.0])
82+
nothing # hide
83+
```
7084

71-
## Interfacing integrator objects
85+
## [Interfacing integrator objects](@id simulation_structure_interfacing_integrators)
7286

73-
During a simulation, the solution is stored in an integrator object, we will here describe how to interface with these. The primary circumstance under which a user may wish to do so is when using [callbacks](@ref advanced_simulations_callbacks). We can create an integrator by calling `init` on our problem ([while circumstances where the user might want to use `init` function exist](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/#Initialization-and-Stepping), since integrators are automatically created during simulations, these are rare):
74-
```@example ex1
87+
During a simulation, the solution is stored in an integrator object. Here, we will describe how to interface with these. The almost exclusive circumstance when integrator-interfacing is relevant is when simulation events are [implemented through callbacks](@ref ref). However, to demonstrate integrator indexing in this tutorial, we will create one through the `init` function (while circumstances where one might [want to use `init` function exist](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/#Initialization-and-Stepping), since integrators are automatically created during simulations, these are rare).
88+
```@example structure_indexing
7589
integrator = init(oprob)
90+
nothing # hide
7691
```
77-
Using a similar syntax to problems, we can get the current values of an unknown within the integrator:
78-
```@example ex1
79-
integrator[:X1]
92+
We can interface with our integrator using an identical syntax as [was used for problems](@ref simulation_structure_interfacing_problems) (with the exception that `remake` is not available). Here we update, and then check the values of, first the species $C$ and then the parameter $k₁$:
93+
```@example structure_indexing
94+
integrator[:C] = 0.0
95+
integrator[:C]
8096
```
8197
or a parameter:
82-
```@example ex1
83-
integrator.ps[:k1]
98+
```@example structure_indexing
99+
integrator.ps[:k₂] = 1.0
100+
integrator.ps[:k₂]
84101
```
85-
Similarly, we can update their values using:
86-
```@example ex1
87-
integrator[:X1] = 10.0
88-
```
89-
Please read [this](@ref advanced_simulations_ssa_callbacks) with regards to updating integrators of `JumpProblem`s.
102+
Note that here, species-interfacing yields (or changes) a simulation's current value for a species, not its initial condition.
90103

104+
If you are interfacing with jump simulation integrators, please read [this, highly relevant, section](@ref ref).
91105

92106
## [Interfacing solution objects](@id simulation_structure_interfacing_solutions)
93107

94108
Finally, we consider solution objects. First, we simulate our problem:
95-
```@example ex1
109+
```@example structure_indexing
96110
sol = solve(oprob)
111+
nothing # hide
97112
```
98-
For solutions, when we access an unknown, we get its whole simulation vector:
99-
```@example ex1
100-
sol[:X1]
113+
Next, we can access the simulation's values using the same notation as previously. When we access a species's, its values across the full simulation is returned as a vector:
114+
```@example structure_indexing
115+
sol[:P]
101116
```
102-
while when we access a parameter we only get a single value:
103-
```@example ex1
104-
sol.ps[:k1]
117+
Parameter values can also be accessed (however, here we only get a single value):
118+
```@example structure_indexing
119+
sol.ps[:k₃]
105120
```
106-
Finally, we note that we cannot change the values of solution unknowns or parameters (i.e. both `sol[:X1] = 0.0` and `sol[:k1] = 0.0` generate errors).
121+
Unlike for problems and integrators, species or parameter values of solutions cannot be changed.
107122

108-
## [Interfacing using symbolic representation](@id simulation_structure_interfacing_symbolic_representation)
123+
A vector with the time values for all simulation time steps can be retrieved using
124+
```@example structure_indexing
125+
sol.t
126+
```
109127

110-
Catalyst is built on an *intermediary representation* implemented by (ModelingToolkit.jl)[https://github.com/SciML/ModelingToolkit.jl]. ModelingToolkit is a modelling framework where one first declares a set of symbolic variables and parameters using e.g.
111-
```@example ex2
112-
using Catalyst # hide
113-
using ModelingToolkit
114-
t = default_t()
115-
@parameters σ ρ β
116-
@variables x(t) y(t) z(t)
117-
nothing # hide
128+
To find simulation values at a specific time point, simply use this time point as input to your solution object (treating it as a function). I.e. here we get our simulation's values at time $t = 1.0$
129+
```@example structure_indexing
130+
sol(1.0)
131+
```
132+
This works whenever the simulations actually stopped at time $t = 1.0$ (if not, an interpolated value is returned). To get the simulation's values for a specific subset of species, we can use the `idxs` optional argument. I.e. here we get the value of $C$ at time $t = 1.0$
133+
```@example structure_indexing
134+
sol(1.0; idxs = [:C])
135+
```
136+
137+
## [Interfacing using specialised getter/setter functions](@id simulation_structure_interfacing_functions)
138+
Internally, species and parameter values are stored in vectors. Whenever e.g. `oprob[:C]` is called, Julia must first find which index in the storage vector $C$ is stored in. Next, its value can be retrieved. If `oprob[:C]` is called a large number of times, this index must be found in each call. If a large number of such accesses are carried out, and performance is essential, it can be worthwhile to pre-compute a function to carry this out.
139+
140+
There exist four different functions, each returning a function for performing a specific type of interfacing:
141+
- `ModelingToolkit.getu`: For accessing species values.
142+
- `ModelingToolkit.getp`: For accessing parameter values.
143+
- `ModelingToolkit.setu`: For changing species values.
144+
- `ModelingToolkit.setp`: For changing parameter values.
145+
146+
For each species (or parameter) we wish to interface with, a new interfacing function must be created. Here we first creates a function for retrieving the value of $C$, and then use it for this purpose:
147+
```@example structure_indexing
148+
get_C = ModelingToolkit.getu(oprob, :C)
149+
get_C(oprob)
150+
```
151+
Here, `getu` (as well as `getp`, `setu`, and `setp`) first takes the structure we wish to interface with, and then the target quantity. When using `setu` and `setp`, in the second step, we must also provide the update value:
152+
```@example structure_indexing
153+
set_C = ModelingToolkit.setu(oprob, :C)
154+
set_C(oprob, 0.2)
155+
get_C(oprob)
118156
```
119-
and then uses these to build systems of equations. Here, these symbolic variables (`x`, `y`, and `z`) and parameters (`σ`, `ρ`, and `β`) can be used to interface a `problem`, `integrator`, and `solution` object (like we did previously, but using Symbols, e.g. `:X`). Since Catalyst models are built on ModelingToolkit, these models also contain similar symbolic variables and parameters.
120-
```@example ex2
157+
158+
Like when indexing-based interfacing is used, these functions also work with vectors:
159+
```@example structure_indexing
160+
get_S = ModelingToolkit.getu(oprob, [:S₁, :S₂])
161+
get_S(oprob)
162+
```
163+
164+
## [Interfacing using symbolic representations](@id simulation_structure_interfacing_symbolic_representation)
165+
As [previously described](@ref ref), when e.g. [programmatic modelling is used](@ref ref), species and parameters can be represented as *symbolic variables*. These can be used to index a problem, just like symbol-based representations can. Here we create a simple [two-state model](@ref ref) programmatically, and use its symbolic variables to check, and update, an initial condition:
166+
```@example structure_indexing_symbolic_variables
121167
using Catalyst
122-
rn = @reaction_network begin
123-
(k1,k2), X1 <--> X2
124-
end
168+
t = default_t()
169+
@species X1(t) X2(t)
170+
@parameters k1 k2
171+
rxs = [
172+
Reaction(k1, [X1], [X2]),
173+
Reaction(k2, [X2], [X1])
174+
]
175+
@named two_state_model = ReactionSystem(rxs, t)
176+
two_state_model = complete(two_state_model)
177+
178+
u0 = [X1 => 2.0, X2 => 0.0]
179+
tspan = (0.0, 1.0)
180+
ps = [k1 => 1.0, k2 => 2.0]
181+
oprob = ODEProblem(two_state_model, u0, tspan, ps)
125182
126-
@unpack k1,k2,X1,X2 = rn
183+
oprob[X1] = 5.0
184+
oprob[X1]
127185
```
128-
Here, we first list the parameters and variables (for reaction systems the latter are typically species) we wish to import (in this case we select all, but we could select only a subset), next we denote from which model (here `rn`) from which we wish to import from. Next, these values can be used directly to interface with e.g. an `ODEProblem`:
129-
```@example ex2
130-
u0 = [X1 => 1.0, X2 => 5.0]
131-
p = [:k1 => 5.0, :k2 => 2.0]
132-
oprob = ODEProblem(rn, u0, (0.0,10.0), p)
186+
Symbolic variables can be used to access or update species or parameters for all the cases when `Symbol`s can (including when using `remake` or e.g. `getu`).
133187

134-
oprob.ps[k1]
188+
An advantage when quantities are represented as symbolic variables is that [symbolic expressions](@ref ref) can be formed and used to index a structure. E.g. here we check the combined initial concentration of $X$ ($X1 + X2$) in our two-state problem:
189+
```@example structure_indexing_symbolic_variables
190+
oprob[X1 + X2]
135191
```
136-
To interface with integrators and solutions we use a similar syntax.
137192

138-
Finally, instead of using `@unpack` to access a symbolic variable or parameter, we can access it directly using `rn.X1`, and thus access an unknown of our `ODEProblem` using
139-
```@example ex2
140-
oprob[rn.X1]
193+
Just like symbolic variables can be used to directly interface with a structure, symbolic variables stored in `ReactionSystem` models can be used:
194+
```@example structure_indexing_symbolic_variables
195+
oprob[two_state_model.X1 + two_state_model.X2]
141196
```
197+
This can be used to form symbolic expressions using model quantities when a model has been created using the DSL (as an alternative to [@unpack]
198+
(@ref ref)). Alternatively, [creating an observable](@ref ref), and then interface using its `Symbol` representation, is also possible.
199+
200+
!!! warn
201+
With interfacing with a simulating structure using symbolic variables stored in a `ReactionSystem` model, ensure that the [model is complete](@ref ref).

0 commit comments

Comments
 (0)