Skip to content

Commit 2713b51

Browse files
committed
init
1 parent d0f1df5 commit 2713b51

File tree

3 files changed

+211
-5
lines changed

3 files changed

+211
-5
lines changed

docs/src/catalyst_functionality/dsl_description.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,58 @@ species(rn)
567567
!!! note
568568
When using interpolation, expressions like `2$spec` won't work; the
569569
multiplication symbol must be explicitly included like `2*$spec`.
570+
571+
## Including observables
572+
Sometimes, one might want to include observable variables. These are variables that can be computed directly from the other system variables (rather than having their values implicitly given through some differential equation). These can be introduced through the `@observables` option.
573+
574+
Let us consider a simple example where two species ($X$ and $Y$) are produced and degraded at constant rates. They can also bind, forming a complex ($XY$). If we want to access the total amount of $X$ in the system we can create an observable that denotes this quantity ($Xtot = X + XY$). Here, we create observables for the total amount of $X$ and $Y$:
575+
```@example obs1
576+
using Catalyst # hide
577+
rn = @reaction_network begin
578+
@observables begin
579+
Xtot ~ X + XY
580+
Ytot ~ Y + XY
581+
end
582+
(pX,dX), 0 <--> X
583+
(pY,dY), 0 <--> Y
584+
(kB,kD), X + Y <--> XY
585+
end
586+
```
587+
The `@observables` option is followed by one line for each observable formula (enclosed by a `begin ... end` block). The left-hand sides indicate the observables' names, and the right-hand sides how their values are computed. The two sides are separated by a `~`.
588+
589+
If we now simulate our model:
590+
```@example obs1
591+
using DifferentialEquations # hide
592+
u0 = [:X => 0.0, :Y => 0.0, :XY => 0.0]
593+
tspan = (0.0, 10.0)
594+
ps = [:pX => 1.0, :dX => 0.2, :pY => 1.0, :dY => 0.5, :kB => 1.0, :kD => 0.2]
595+
oprob = ODEProblem(rn, u0, tspan, ps)
596+
sol = solve(oprob)
597+
nothing # hide
598+
```
599+
we can index the solution using our observables (just like for [other variables](@ref simulation_structure_interfacing_solutions)). E.g. we can receive a vector with all $Xtot$ values using
600+
```@example obs1
601+
sol[:Xtot]
602+
```
603+
similarly, we can plot the values of $Xtot$ and $Ytot$ using
604+
```@example obs1
605+
plot(sol; idxs=[:Xtot, :Ytot])
606+
```
607+
608+
If we only wish to provide a single observable, the `begin ... end` block is note required. E.g., to record only the total amount of $X$ we can use:
609+
```@example obs1
610+
using Catalyst # hide
611+
rn = @reaction_network begin
612+
@observables Xtot ~ X + XY
613+
(pX,dX), 0 <--> X
614+
(pY,dY), 0 <--> Y
615+
(kB,kD), X + Y <--> XY
616+
end
617+
```
618+
619+
Finally, some general rules for creating observables:
620+
- Observables can depend on any species, parameters, or variables, but not on other observables.
621+
- All observables components must be declared somewhere (i.e., they cannot only appear as a part of the observables formula).
622+
- Only a single `@observables` option block can be used in each `@reaction_network` call.
623+
- The left-hand side of the observables expression must be a single symbol, indicating the observable's name.
624+
- The right-hand side of the observables expression can be any valid algebraic expression.

src/reaction_network.jl

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const forbidden_variables_error = let
123123
end
124124

125125
# Declares the keys used for various options.
126-
const option_keys = (:species, :parameters, :variables, :ivs, :compounds)
126+
const option_keys = (:species, :parameters, :variables, :ivs, :compounds, :observables)
127127

128128
### The @species macro, basically a copy of the @variables macro. ###
129129
macro species(ex...)
@@ -358,8 +358,9 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))
358358
options = Dict(map(arg -> Symbol(String(arg.args[1])[2:end]) => arg,
359359
option_lines))
360360

361-
# Reads compounds options.
361+
# Reads options.
362362
compound_expr, compound_species = read_compound_options(options)
363+
observed_vars, observed_eqs = read_observed_options(options)
363364

364365
# Parses reactions, species, and parameters.
365366
reactions = get_reactions(reaction_lines)
@@ -414,11 +415,12 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))
414415
$ivexpr
415416
$vars
416417
$sps
418+
$observed_vars
417419
$comps
418420

419421
Catalyst.make_ReactionSystem_internal($rxexprs, $tiv, union($spssym, $varssym, $compssym),
420-
$pssym; name = $name,
421-
spatial_ivs = $sivs)
422+
$pssym; name = $name, spatial_ivs = $sivs,
423+
observed = $observed_eqs)
422424
end
423425
end
424426

@@ -682,6 +684,46 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues,
682684
reactants
683685
end
684686

687+
### DSL Options Handling ###
688+
# Most options handled in previous sections, when code re-organised, these should ideally be moved to the same place.
689+
690+
# Reads the observables options. Outputs an expression ofr creating the obervable variables, and a vector of observable equations.
691+
function read_observed_options(options)
692+
if haskey(options, :observables)
693+
# Gets list of observable equations and prepares variable declaration expression.
694+
# (`options[:observables]` inlucdes `@observables`, `.args[3]` removes this part)
695+
observed_eqs = make_observed_eqs(options[:observables].args[3])
696+
observed_vars = :(@variables)
697+
698+
# For each observable, checks for errors and adds the variable to `observed_vars`.
699+
for obs_eq in observed_eqs.args
700+
(obs_eq.args[1] != :~) && error("Malformed observable formula :\"$(obs_eq)\". Formula should contain two expressions separated by a '~'.")
701+
(obs_eq.args[2] isa Symbol) || error("Malformed observable formula :\"$(obs_eq)\". Left hand side should be a single symbol.")
702+
push!(observed_vars.args, obs_eq.args[2])
703+
end
704+
else
705+
# If option is not used, return empty expression and vector.
706+
observed_vars = :()
707+
observed_eqs = :([])
708+
end
709+
return observed_vars, observed_eqs
710+
end
711+
712+
# From the input to the @observables options, creates a vector containing one equation for each observable.
713+
# Checks separate cases for "@obervables O ~ ..." and "@obervables begin ... end". Other cases errors.
714+
function make_observed_eqs(observables_expr)
715+
if observables_expr.head == :call
716+
return :([$(observables_expr)])
717+
elseif observables_expr.head == :block
718+
observed_eqs = :([])
719+
for arg in observables_expr.args
720+
push!(observed_eqs.args, arg)
721+
end
722+
return observed_eqs
723+
else
724+
error("Malformed observables option usage: $(observables_expr).")
725+
end
726+
end
685727
### Functionality for expanding function call to custom and specific functions ###
686728

687729
#Recursively traverses an expression and replaces special function call like "hill(...)" with the actual corresponding expression.

test/dsl/dsl_options.jl

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#! format: off
22

33
### Fetch Packages and Set Global Variables ###
4-
using Catalyst, ModelingToolkit
4+
using Catalyst, ModelingToolkit, Plots
55
@variables t
66

77
### Run Tests ###
@@ -339,3 +339,112 @@ let
339339
@test ModelingToolkit.defaults(rn27) == defs29
340340
@test merge(ModelingToolkit.defaults(rn28), defs28) == ModelingToolkit.defaults(rn27)
341341
end
342+
343+
### Observables ###
344+
345+
# Test basic functionality.
346+
# Tests various types of indexing.
347+
let
348+
rn = @reaction_network begin
349+
@observables begin
350+
X ~ Xi + Xa
351+
Y ~ Y1 + Y2
352+
end
353+
(p,d), 0 <--> Xi
354+
(k1,k2), Xi <--> Xa
355+
(k3,k4), Y1 <--> Y2
356+
end
357+
@unpack X, Xi, Xa, Y, Y1, Y2, p, d, k1, k2, k3, k4 = rn
358+
359+
# Test that ReactionSystem have the correct properties.
360+
@test length(species(rn)) == 4
361+
@test length(states(rn)) == 4
362+
@test length(observed(rn)) == 2
363+
@test length(equations(rn)) == 6
364+
365+
@test isequal(observed(rn)[1], X ~ Xi + Xa)
366+
@test isequal(observed(rn)[2], Y ~ Y1 + Y2)
367+
368+
# Tests correct indexing of solution.
369+
u0 = [Xi => 0.0, Xa => 0.0, Y1 => 1.0, Y2 => 2.0]
370+
ps = [p => 1.0, d => 0.2, k1 => 1.5, k2 => 1.5, k3 => 5.0, k4 => 5.0]
371+
372+
oprob = ODEProblem(rn, u0, (0.0, 1000.0), ps)
373+
sol = solve(oprob, Tsit5())
374+
@test sol[X][end] 10.0
375+
@test sol[Y][end] 3.0
376+
@test sol[rn.X][end] 10.0
377+
@test sol[rn.Y][end] 3.0
378+
@test sol[:X][end] 10.0
379+
@test sol[:Y][end] 3.0
380+
381+
# Tests that observables can be used for plot indexing.
382+
@test plot(sol; idxs=X).series_list[1].plotattributes[:y][end] 2.5
383+
@test plot(sol; idxs=rn.X).series_list[1].plotattributes[:y][end] 2.5
384+
@test plot(sol; idxs=:X).series_list[1].plotattributes[:y][end] 2.5
385+
@test plot(sol; idxs=[X, Y]).series_list[2].plotattributes[:y][end] 3.0
386+
@test plot(sol; idxs=[rn.X, rn.Y]).series_list[2].plotattributes[:y][end] 3.0
387+
@test plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] 3.0
388+
end
389+
390+
# Tests for complicated observable formula.
391+
# Tests using a single observable (without begin/end statement).
392+
# Tests using observable component not part of reaction.
393+
# Tests using parameters in observables formula.
394+
let
395+
rn = @reaction_network begin
396+
@parameters op_1 op_2
397+
@species X4(t)
398+
@observables X ~ X1^2 + op_1*(X2 + 2X3) + X1*X4/op_2 + p
399+
(p,d), 0 <--> X1
400+
(k1,k2), X1 <--> X2
401+
(k3,k4), X2 <--> X3
402+
end
403+
404+
u0 = Dict([:X1 => 1.0, :X2 => 2.0, :X3 => 3.0, :X4 => 4.0])
405+
ps = Dict([:p => 1.0, :d => 0.2, :k1 => 1.5, :k2 => 1.5, :k3 => 5.0, :k4 => 5.0, :op_1 => 1.5, :op_2 => 1.5])
406+
407+
oprob = ODEProblem(rn, u0, (0.0, 1000.0), ps)
408+
sol = solve(oprob, Tsit5())
409+
410+
@test sol[:X][1] == u0[:X1]^2 + ps[:op_1]*(u0[:X2] + 2*u0[:X3]) + u0[:X1]*u0[:X4]/ps[:op_2] + ps[:p]
411+
end
412+
413+
# Tests various erroneous declarations throw errors.
414+
let
415+
# System with undeclared component as observable.
416+
@test_throws Exception @eval @reaction_network begin
417+
@observables begin
418+
X ~ X1 + X2
419+
end
420+
(p,d), 0 <--> X1
421+
end
422+
423+
# System with observable in observable formula.
424+
@test_throws Exception @eval @reaction_network begin
425+
@observables begin
426+
X ~ X1 + X2
427+
X2 ~ 2X
428+
end
429+
(p,d), 0 <--> X1 + X2
430+
end
431+
432+
# System with multiple observables blocks.
433+
@test_throws Exception @eval @reaction_network begin
434+
@observables begin
435+
X ~ X1 + X2
436+
end
437+
@observables begin
438+
X2 ~ 2(X1 + X2)
439+
end
440+
(p,d), 0 <--> X1 + X2
441+
end
442+
443+
# System with no trivial observable left-hand side.
444+
@test_throws Exception @eval @reaction_network begin
445+
@observables begin
446+
X + X2 ~ 2X1
447+
end
448+
(p,d), 0 <--> X1 + X2
449+
end
450+
end

0 commit comments

Comments
 (0)