Skip to content

Commit 1f08118

Browse files
committed
up
1 parent d9d5eab commit 1f08118

File tree

5 files changed

+104
-30
lines changed

5 files changed

+104
-30
lines changed

docs/src/catalyst_functionality/dsl_description.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,13 @@ Finally, some general rules for creating observables:
623623
- The left-hand side of the observables expression must be a single symbol, indicating the observable's name.
624624
- Metadata can, however, be provided, e.g through `@observables (Xtot, [description="Total amount of X"]) ~ X + XY`.
625625
- The right-hand side of the observables expression can be any valid algebraic expression.
626-
- Observables are (by default, but this can be changed) considered `variables` (and not `species`). This can be changed by e.g. pre-declaring them using the `@species` option.
626+
- Observables are (by default, but this can be changed) considered `variables` (and not `species`). This can be changed by e.g. pre-declaring them using the `@species` option:
627+
```@example obs2
628+
using Catalyst # hide
629+
rn = @reaction_network begin
630+
@species Xtot(t)
631+
@observables Xtot ~ X1 + X2
632+
(k1,k2), X1 <--> X2
633+
end
634+
nothing # hide
635+
```

src/expression_utils.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ end
2424
# The independent variables are given as a vector (empty if none given).
2525
# Does not support e.g. "X [metadata=true]" (when metadata does not have a comma before).
2626
function find_varinfo_in_declaration(expr)
27+
# Handles the $(Expr(:escape, :Y)) case:
28+
is_escaped_expr(expr) && (return find_varinfo_in_declaration(expr.args[1]))
29+
2730
# Case: X
2831
(expr isa Symbol) && (return expr, [], nothing, nothing)
2932
# Case: X(t)
@@ -84,6 +87,10 @@ function insert_independent_variable(expr_in, iv_expr)
8487
return expr
8588
end
8689

90+
# Checks if an expression is an escaped expression (e.g. on the form `$(Expr(:escape, :Y))`)
91+
function is_escaped_expr(expr)
92+
return (expr isa Expr) && (expr.head == :escape) && (length(expr.args) == 1)
93+
end
8794

8895
### Old Stuff ###
8996

src/reaction_network.jl

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))
382382
all_ivs = (isnothing(sivs) ? [tiv] : [tiv; sivs.args])
383383

384384
# Reads more options.
385-
observed_vars, observed_eqs = read_observed_options(options, [species_declared; variables], all_ivs)
386-
385+
observed_vars, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], all_ivs)
386+
387387
declared_syms = Set(Iterators.flatten((parameters_declared, species_declared,
388388
variables)))
389389
species_extracted, parameters_extracted = extract_species_and_parameters!(reactions,
@@ -421,7 +421,7 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))
421421
$observed_vars
422422
$comps
423423

424-
Catalyst.make_ReactionSystem_internal($rxexprs, $tiv, union($spssym, $varssym, $compssym),
424+
Catalyst.make_ReactionSystem_internal($rxexprs, $tiv, setdiff(union($spssym, $varssym, $compssym), $obs_syms),
425425
$pssym; name = $name, spatial_ivs = $sivs,
426426
observed = $observed_eqs)
427427
end
@@ -691,49 +691,60 @@ end
691691
# Most options handled in previous sections, when code re-organised, these should ideally be moved to the same place.
692692

693693
# Reads the observables options. Outputs an expression ofr creating the obervable variables, and a vector of observable equations.
694-
function read_observed_options(options, species_declared, ivs_sorted)
694+
function read_observed_options(options, species_n_vars_declared, ivs_sorted)
695695
if haskey(options, :observables)
696696
# Gets list of observable equations and prepares variable declaration expression.
697697
# (`options[:observables]` inlucdes `@observables`, `.args[3]` removes this part)
698698
observed_eqs = make_observed_eqs(options[:observables].args[3])
699699
observed_vars = Expr(:block, :(@variables))
700-
700+
obs_syms = :([])
701+
701702
for (idx, obs_eq) in enumerate(observed_eqs.args)
702703
# Extract the observable, checks errors, and continues the loop if the observable has been declared.
703704
obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2])
704705
isempty(ivs) || error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.")
705706
isnothing(defaults) || error("An observable ($obs_name) was given a default value. This is forbidden.")
706707
in(obs_name, forbidden_symbols_error) && error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.")
707-
if (obs_name in species_declared)
708+
if (obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])
708709
isnothing(metadata) || error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the obervable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.")
709-
continue
710710
end
711711

712-
# Appends (..) to the observable (which is later replaced with the extracted ivs).
713-
# Adds the observable to the first line of the output expression (starting with `@variables`).
714-
obs_expr = insert_independent_variable(obs_eq.args[2], :(..))
715-
push!(observed_vars.args[1].args, obs_expr)
716-
717-
# Adds a line to the `observed_vars` expression, setting the ivs for this observable.
718-
# Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because
719-
# then we get something like :([:X1, :X2]), rather than :([X1, X2]).
720-
dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3]))))
721-
ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr])))
722-
ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted)))
723-
push!(observed_vars.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...)))
724-
725-
# In case metadata was given, this must be cleared from `observed_eqs`
726-
observed_eqs.args[idx].args[2] = obs_name
712+
# This bits adds the observables to the @variables vector which is given as output.
713+
# For Observables that have already been declared using @species/@variables,
714+
# or are interpolated, this parts should not be carried out.
715+
if !((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2]))
716+
# Appends (..) to the observable (which is later replaced with the extracted ivs).
717+
# Adds the observable to the first line of the output expression (starting with `@variables`).
718+
obs_expr = insert_independent_variable(obs_eq.args[2], :(..))
719+
push!(observed_vars.args[1].args, obs_expr)
720+
721+
# Adds a line to the `observed_vars` expression, setting the ivs for this observable.
722+
# Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because
723+
# then we get something like :([:X1, :X2]), rather than :([X1, X2]).
724+
dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3]))))
725+
ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr])))
726+
ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted)))
727+
push!(observed_vars.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...)))
728+
end
729+
730+
# In case metadata was given, this must be cleared from `observed_eqs`.
731+
# For interpolated observables (I.e. $X ~ ...) this should and cannot be done.
732+
is_escaped_expr(obs_eq.args[2]) || (observed_eqs.args[idx].args[2] = obs_name)
733+
734+
# Adds the observable to the list of observable names.
735+
# This is required for filtering away so these are not added to the ReactionSystem's species list.
736+
push!(obs_syms.args, obs_name)
727737
end
728-
738+
729739
# If nothing was added to `observed_vars`, it has to be modified not to throw an error.
730740
(length(observed_vars.args) == 1) && (observed_vars = :())
731741
else
732742
# If option is not used, return empty expression and vector.
733743
observed_vars = :()
734744
observed_eqs = :([])
745+
obs_syms = :([])
735746
end
736-
return observed_vars, observed_eqs
747+
return observed_vars, observed_eqs, obs_syms
737748
end
738749

739750
# From the input to the @observables options, creates a vector containing one equation for each observable.

src/reactionsystem.jl

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,7 @@ struct ReactionSystem{V <: NetworkProperties} <:
516516
function ReactionSystem(eqs, rxs, iv, sivs, states, spcs, ps, var_to_name, observed,
517517
name, systems, defaults, connection_type, nps, cls, cevs, devs,
518518
complete::Bool = false; checks::Bool = true)
519-
# Filters away any potential obervables from `states` and `spcs`.
520-
obs_vars = [obs_eq.lhs for obs_eq in observed]
521-
states = filter(state -> !any(isequal(state, obs_var) for obs_var in obs_vars), states)
522-
spcs = filter(spc -> !any(isequal(spc, obs_var) for obs_var in obs_vars), spcs)
523-
519+
524520
# unit checks are for ODEs and Reactions only currently
525521
nonrx_eqs = Equation[eq for eq in eqs if eq isa Equation]
526522
if checks && isempty(sivs)

test/dsl/dsl_options.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,57 @@ let
510510
@test isequal(states(rn3), states(rn4))
511511
end
512512

513+
# Tests for interpolation into the observables option.
514+
let
515+
# Interpolation into lhs.
516+
@species X [description="An observable"]
517+
rn1 = @reaction_network begin
518+
@observables $X ~ X1 + X2
519+
(k1, k2), X1 <--> X2
520+
end
521+
@test isequal(observed(rn1)[1].lhs, X)
522+
@test getdescription(rn1.X) == "An observable"
523+
@test isspecies(rn1.X)
524+
@test length(states(rn1)) == 2
525+
526+
# Interpolation into rhs.
527+
@parameters n [description="A parameter"]
528+
@variables t
529+
@species S(t)
530+
rn2 = @reaction_network begin
531+
@observables Stot ~ $S + $n*Sn
532+
(kB, kD), $n*S <--> Sn
533+
end
534+
@unpack Stot, Sn, kD, kB = rn2
535+
536+
u0 = Dict([S => 5.0, Sn => 1.0])
537+
ps = Dict([n => 2, kB => 1.0, kD => 1.0])
538+
oprob = ODEProblem(rn2, u0, (0.0, 1.0), ps)
539+
540+
@test issetequal(Symbolics.get_variables(observed(rn2)[1].rhs), [S, n, Sn])
541+
@test oprob[Stot] == u0[S] + ps[n]*u0[Sn]
542+
@test length(states(rn2)) == 2
543+
end
544+
545+
# Tests specific declaration of Observables as species/variables
546+
let
547+
rn = @reaction_network begin
548+
@species X(t)
549+
@variables Y(t)
550+
@observables begin
551+
X ~ X + 2X2
552+
Y ~ Y1 + Y2
553+
Z ~ X + Y
554+
end
555+
(kB,kD), 2X <--> X2
556+
(k1,k2), Y1 <--> Y2
557+
end
558+
559+
@test isspecies(rn.X)
560+
@test !isspecies(rn.Y)
561+
@test !isspecies(rn.Z)
562+
end
563+
513564
# Tests various erroneous declarations throw errors.
514565
let
515566
# Independent variable in @observables.

0 commit comments

Comments
 (0)