From b576c0dc1a25dbca2af3dadd576a42701bfb4e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Fri, 8 Sep 2023 03:42:38 +0200 Subject: [PATCH 01/11] agentize state, observables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bíma, Jan --- LICENSE | 2 +- LICENSES_THIRD_PARTY | 36 +--- Project.toml | 36 ++-- src/ReactiveDynamics.jl | 2 +- src/compilers.jl | 2 +- src/interface/reaction_parser.jl | 2 +- src/interface/solve.jl | 277 ++----------------------------- src/solvers.jl | 32 +--- src/state.jl | 120 ++++++------- test/tutorial_tests.jl | 4 +- tutorial/basics.jl | 2 +- 11 files changed, 109 insertions(+), 406 deletions(-) diff --git a/LICENSE b/LICENSE index 1f44d4d..436ac3c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright © 2022 Merck & Co., Inc., Rahway, NJ, USA and its affiliates. All rights reserved. +Copyright © 2023 Merck & Co., Inc., Rahway, NJ, USA and its affiliates. All rights reserved. Jan Bima (MSD), Otto Ritter (MSD), Sean L. Wu (Merck) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index ee51c37..7d07699 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -1,40 +1,10 @@ -ReactiveDynamics.jl depends on third-party Julia packages which may be distributed under different licenses. We have listed all of these third party packages and their licenses below. For the most up-to-date information, see `Project.toml`. +ReactiveDynamics.jl depends on third-party Julia packages, which may be distributed under various licenses. For the most recent list of these packages, refer to `Project.toml` and consult the license terms of the individual packages. -You must agree to the terms of these licenses, in addition to the ReactiveDynamics source code license, in order to use this software. +You must agree to the terms of these licenses, in addition to the ReactiveDynamics.jl source code license, in order to use this software. -------------------------------------------------- Third party software listed by License type -------------------------------------------------- MIT License (or adaptations) (https://www.opensource.org/licenses/MIT) - * The Julia Language - https://github.com/JuliaLang/julia/blob/master/LICENSE.md - * BenchmarkTools.jl - https://github.com/JuliaCI/BenchmarkTools.jl - * CSV - https://github.com/JuliaData/CSV.jl - * Catlab - https://github.com/AlgebraicJulia/Catlab.jl - * ComponentArrays.jl - https://github.com/jonniedie/ComponentArrays.jl - * Crayons - https://github.com/KristofferC/Crayons.jl - * DataFrames.jl - https://github.com/JuliaData/DataFrames.jl - * DiffEqBase.jl - https://github.com/SciML/DiffEqBase.jl - * DifferentialEquations.jl - https://github.com/SciML/DifferentialEquations.jl - * Distributions.jl - https://github.com/JuliaStats/Distributions.jl - * Documenter - https://github.com/JuliaDocs/Documenter.jl - * DocumenterMarkdown - https://github.com/JuliaDocs/DocumenterMarkdown.jl - * GeneratedExpressions.jl - https://github.com/Merck/GeneratedExpressions.jl - * IJulia - https://github.com/JuliaLang/IJulia.jl - * JLD2 - https://github.com/JuliaIO/JLD2.jl - * JSON.jl - https://github.com/JuliaIO/JSON.jl - * MacroTools.jl - https://github.com/FluxML/MacroTools.jl - * NLopt - https://github.com/JuliaOpt/NLopt.jl - * OrdinaryDiffEq - https://github.com/SciML/OrdinaryDiffEq.jl - * Plots - https://github.com/JuliaPlots/Plots.jl - * Pluto.jl - https://github.com/fonsp/Pluto.jl - * Reexport - https://github.com/simonster/Reexport.jl - * SafeTestsets - https://github.com/YingboMa/SafeTestsets.jl - * Statistics.jl - https://github.com/JuliaStats/Statistics.jl - * StatsFuns.jl - https://github.com/JuliaStats/StatsFuns.jl - * Symbolics.jl - https://github.com/JuliaSymbolics/Symbolics.jl - * TOML.jl - https://github.com/JuliaLang/TOML.jl - * Tables.jl - https://github.com/JuliaData/Tables.jl - -The Unlicense (https://opensource.org/licenses/unlicense) - * PlutoUI.jl - https://github.com/JuliaPluto/PlutoUI.jl \ No newline at end of file + * The Julia Language - https://github.com/JuliaLang/julia/blob/master/LICENSE.md \ No newline at end of file diff --git a/Project.toml b/Project.toml index b1a51d5..354298d 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,7 @@ uuid = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4" version = "0.2.7" [deps] +AlgebraicAgents = "f6eb0ae3-10fa-40e6-88dd-9006ba45093a" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" @@ -23,6 +24,7 @@ OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -33,31 +35,31 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -julia = "1.9" -DifferentialEquations = "7.9" -StatsFuns = "1.3" +CSV = "0.10" Catlab = "0.14" +ComponentArrays = "0.14" +Crayons = "4.1" DataFrames = "1.6" -PlutoUI = "0.7" -Statistics = "1.9" +DiffEqBase = "6.128" +DifferentialEquations = "7.9" +Distributions = "0.25" +Documenter = "0.27" DocumenterMarkdown = "0.2" -ComponentArrays = "0.14" -JLD2 = "0.4" GeneratedExpressions = "0.1" -DiffEqBase = "6.128" +IJulia = "1.24" +JLD2 = "0.4" JSON = "0.21" +MacroTools = "0.5" NLopt = "1.0" OrdinaryDiffEq = "6.55" -Symbolics = "5.5" -IJulia = "1.24" -SafeTestsets = "0.1" -CSV = "0.10" Plots = "1.39" +Pluto = "0.19" +PlutoUI = "0.7" Reexport = "1.2" +SafeTestsets = "0.1" +Statistics = "1.9" +StatsFuns = "1.3" +Symbolics = "5.5" TOML = "1.0" -MacroTools = "0.5" -Crayons = "4.1" -Documenter = "0.27" Tables = "1.10" -Distributions = "0.25" -Pluto = "0.19" \ No newline at end of file +julia = "1.9" diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl index 083152f..a723d9a 100644 --- a/src/ReactiveDynamics.jl +++ b/src/ReactiveDynamics.jl @@ -220,7 +220,7 @@ include.(readdir(joinpath(@__DIR__, "interface"); join = true)) include.(readdir(joinpath(@__DIR__, "utils"); join = true)) include.(readdir(joinpath(@__DIR__, "operators"); join = true)) include("solvers.jl") -include("optim.jl") +#include("optim.jl") include("loadsave.jl") end diff --git a/src/compilers.jl b/src/compilers.jl index d1b5314..4588b69 100644 --- a/src/compilers.jl +++ b/src/compilers.jl @@ -112,7 +112,7 @@ function wrap_expr(fex, species_names, prm_names, varmap) ) push!(letex.args[2].args, fex) - # the function shall be a function of the dynamic ReactiveDynamicsState structure: letex -> :(state -> $letex) + # the function shall be a function of the dynamic ReactiveNetwork structure: letex -> :(state -> $letex) # eval the expression to a Julia function, save that function into the "compiled" acset return eval(:(state -> $letex)) end diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl index ae92ae3..df69f53 100644 --- a/src/interface/reaction_parser.jl +++ b/src/interface/reaction_parser.jl @@ -29,7 +29,7 @@ function recursively_choose(r_line, state) end end -function extract_reactants(r_line, state::ReactiveDynamicsState) +function extract_reactants(r_line, state::ReactiveNetwork) r_line = recursively_choose(r_line, state) return recursive_find_reactants!( diff --git a/src/interface/solve.jl b/src/interface/solve.jl index a7484e9..029aee3 100644 --- a/src/interface/solve.jl +++ b/src/interface/solve.jl @@ -1,26 +1,25 @@ -export @problematize, @solve, @plot +export @agentize, @solve, @plot export @optimize, @fit, @fit_and_plot, @build_solver -using DifferentialEquations: DiscreteProblem, EnsembleProblem, FunctionMap, EnsembleSolution import MacroTools import Plots """ -Convert a model to a `DiscreteProblem`. If passed a problem instance, return the instance. +Convert a model to a `ReactiveNetwork`. If passed a problem instance, return the instance. # Examples ```julia -@problematize acs tspan = 1:100 +@agentize acs tspan = 1:100 ``` """ -macro problematize(acsex, args...) +macro agentize(acsex, args...) args, kwargs = args_kwargs(args) quote - if $(esc(acsex)) isa DiscreteProblem + if $(esc(acsex)) isa ReactiveNetwork $(esc(acsex)) else - DiscreteProblem($(esc(acsex)), $(args...); $(kwargs...)) + ReactiveNetwork($(esc(acsex)), $(args...); $(kwargs...)) end end end @@ -33,7 +32,7 @@ Solve the problem. Solverargs passed at the calltime take precedence. ```julia @solve prob @solve prob tspan = 1:100 -@solve prob tspan = 100 trajectories = 20 +@solve prob tspan = 100 ``` """ macro solve(probex, args...) @@ -42,20 +41,13 @@ macro solve(probex, args...) !isnothing(findfirst(el -> el.args[1] == :trajectories, kwargs)) && (mode = :ensemble) quote - prob = if $(esc(probex)) isa DiscreteProblem + prob = if $(esc(probex)) isa ReactiveNetwork $(esc(probex)) else - DiscreteProblem($(esc(probex)), $(args...); $(kwargs...)) - end - if $(preserve_sym(mode)) == :ensemble - solve( - EnsembleProblem(prob; prob_func = get_prob_func(prob)), - FunctionMap(), - $(kwargs...), - ) - else - solve(prob) + ReactiveNetwork($(esc(probex)), $(args...); $(kwargs...)) end + + simulate(prob) end end @@ -254,249 +246,4 @@ function plot_from_log(state, record_type, ixs; kwargs...) label = reshape(label, 1, :), kwargs..., ) -end - -""" - @optimize acset objective ... ... opts... - -Take an acset and optimize given functional. - -Objective is an expression which may reference the model's variables and parameters, i.e., `A+β`. -The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. -The vector of free variables passed to the `NLopt` solver has the form `[free_vars; free_params]`; order of vars and params, respectively, is preserved. - -By default, the functional is minimized. Specify `objective=max` to perform maximization. - -Propagates `NLopt` solver arguments; see [NLopt documentation](https://github.com/JuliaOpt/NLopt.jl). - -# Examples - -```julia -@optimize acs abs(A - B) A B = 20.0 α = 2.0 lower_bounds = 0 upper_bounds = 100 -@optimize acss abs(A - B) A B = 20.0 α = 2.0 upper_bounds = [200, 300, 400] maxeval = 200 objective = - min -``` -""" -macro optimize(acsex, obex, args...) - args_all = args - args, kwargs = args_kwargs(args) - min_t = find_kwargex_delete!(kwargs, :min_t, -Inf) - max_t = find_kwargex_delete!(kwargs, :max_t, Inf) - final_only = find_kwargex_delete!(kwargs, :final_only, false) - okwargs = filter(ex -> ex.args[1] in [:loss, :trajectories], kwargs) - - quote - u0, p = get_free_vars($(esc(acsex)), $(QuoteNode(args_all))) - prob_ = DiscreteProblem($(esc(acsex))) - prep_u0!(u0, prob_) - prep_params!(p, prob_) - - init_p = [k => v for (k, v) in p] - init_vec = if length(u0) > 0 - ComponentVector{Float64}(; species = collect(wvalues(u0)), init_p...) - else - ComponentVector{Float64}(; init_p...) - end - - o = build_loss_objective( - $(esc(acsex)), - init_vec, - u0, - p, - $(QuoteNode(obex)); - min_t = $min_t, - max_t = $max_t, - final_only = $final_only, - $(okwargs...), - ) - - optim!(o, init_vec; $(kwargs...)) - end -end - -""" - @fit acset data_points time_steps empiric_variables ... ... opts... - -Take an acset and fit initial values and parameters to empirical data. - -The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. -The vector of free variables passed to the `NLopt` solver has the form `[free_vars; free_params]`; order of vars and params, respectively, is preserved. - -Propagates `NLopt` solver arguments; see [NLopt documentation](https://github.com/JuliaOpt/NLopt.jl). - -# Examples - -```julia -t = [1, 50, 100] -data = [80 30 20] -@fit acs data t vars = A B = 20 A α # fit B, A, α; empirical data is for variable A -``` -""" -macro fit(acsex, data, t, args...) - args_all = args - args, kwargs = args_kwargs(args) - okwargs = filter(ex -> ex.args[1] in [:loss, :trajectories], kwargs) - vars = (ix = findfirst(ex -> ex.args[1] == :vars, kwargs); - !isnothing(ix) ? (v = kwargs[ix].args[2]; - deleteat!(kwargs, ix); - v) : :()) - - quote - u0, p = get_free_vars($(esc(acsex)), $(QuoteNode(args_all))) - vars = get_vars($(esc(acsex)), $(QuoteNode(vars))) - prob_ = DiscreteProblem($(esc(acsex))) - prep_u0!(u0, prob_) - prep_params!(p, prob_) - - init_p = [k => v for (k, v) in p] - init_vec = if length(u0) > 0 - ComponentVector{Float64}(; species = collect(wvalues(u0)), init_p...) - else - ComponentVector{Float64}(; init_p...) - end - - o = build_loss_objective_datapoints( - $(esc(acsex)), - init_vec, - u0, - p, - $(esc(t)), - $(esc(data)), - vars; - $(okwargs...), - ) - - optim!(o, init_vec; $(kwargs...)) - end -end - -""" - @fit acset data_points time_steps empiric_variables ... ... opts... - -Take an acset, fit initial values and parameters to empirical data, and plot the result. - -The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. -The vector of free variables passed to the `NLopt` solver has the form `[free_vars; free_params]`; order of vars and params, respectively, is preserved. - -Propagates `NLopt` solver arguments; see [NLopt documentation](https://github.com/JuliaOpt/NLopt.jl). - -# Examples - -```julia -t = [1, 50, 100] -data = [80 30 20] -@fit acs data t vars = A B = 20 A α # fit B, A, α; empirical data is for variable A -``` -""" -macro fit_and_plot(acsex, data, t, args...) - args_all = args - trajectories = get_kwarg(args, :trajectories, 1) - args, kwargs = args_kwargs(args) - okwargs = filter(ex -> ex.args[1] in [:loss, :trajectories], kwargs) - vars = (ix = findfirst(ex -> ex.args[1] == :vars, kwargs); - !isnothing(ix) ? (v = kwargs[ix].args[2]; - deleteat!(kwargs, ix); - v) : :()) - - quote - u0, p = get_free_vars($(esc(acsex)), $(QuoteNode(args_all))) - vars = get_vars($(esc(acsex)), $(QuoteNode(vars))) - prob_ = DiscreteProblem($(esc(acsex)); suppress_warning = true) - prep_u0!(u0, prob_) - prep_params!(p, prob_) - - init_p = [k => v for (k, v) in p] - init_vec = if length(u0) > 0 - ComponentVector{Float64}(; species = collect(wvalues(u0)), init_p...) - else - ComponentVector{Float64}(; init_p...) - end - - o = build_loss_objective_datapoints( - $(esc(acsex)), - init_vec, - u0, - p, - $(esc(t)), - $(esc(data)), - vars; - $(okwargs...), - ) - - r = optim!(o, init_vec; $(kwargs...)) - if r[3] != :FORCED_STOP - s_ = build_parametrized_solver( - $(esc(acsex)), - init_vec, - u0, - p; - trajectories = $trajectories, - ) - sol = first(s_(init_vec)) - sol_ = first(s_(r[2])) - - p = Plots.plot( - sol; - idxs = vars, - label = "(initial) " .* - reshape(String.($(esc(acsex))[:, :specName])[vars], 1, :), - ) - Plots.plot!( - p, - $(esc(t)), - transpose($(esc(data))); - label = "(empirical) " .* - reshape(String.($(esc(acsex))[:, :specName])[vars], 1, :), - ) - Plots.plot!( - p, - sol_; - idxs = vars, - label = "(fitted) " .* - reshape(String.($(esc(acsex))[:, :specName])[vars], 1, :), - ) - p - else - :FORCED_STOP - end - end -end - -""" - @build_solver acset ... ... opts... - -Take an acset and export a solution as a function of free vars and free parameters. - -# Examples - -```julia -solver = @build_solver acs S α β # function of variable S and parameters α, β -solver([S, α, β]) -``` -""" -macro build_solver(acsex, args...) - args_all = args#; args, kwargs = args_kwargs(args) - trajectories = get_kwarg(args, :trajectories, 1) - - quote - u0, p = get_free_vars($(esc(acsex)), $(QuoteNode(args_all))) - prob_ = DiscreteProblem($(esc(acsex))) - prep_u0!(u0, prob_) - prep_params!(p, prob_) - - init_p = [k => v for (k, v) in p] - init_vec = if length(u0) > 0 - ComponentVector{Float64}(; species = collect(wvalues(u0)), init_p...) - else - ComponentVector{Float64}(; init_p...) - end - - build_parametrized_solver_( - $(esc(acsex)), - init_vec, - u0, - p; - trajectories = $trajectories, - ) - end -end +end \ No newline at end of file diff --git a/src/solvers.jl b/src/solvers.jl index e2f22f1..ddddb86 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -4,6 +4,7 @@ export DiscreteProblem using DiffEqBase, DifferentialEquations using Distributions +using Random function get_sampled_transition(state, i) transition = Dict{Symbol,Any}() @@ -166,7 +167,7 @@ function evolve!(u, state) for i = 1:nparts(state, :T) qs[i] != 0 && push!( state.ongoing_transitions, - Transition(get_sampled_transition(state, i), state.t, qs[i], 0.0), + Transition(state[i, :transName] * "_@$(state.t)", get_sampled_transition(state, i), state.t, qs[i], 0.0), ) end @@ -308,7 +309,7 @@ transform(DiscreteProblem, acs; schedule = schedule_weighted!) """ function transform( ::Type{DiffEqBase.DiscreteProblem}, - state::ReactiveDynamicsState; + state::ReactiveNetwork; kwargs..., ) f = function (du, u, p, t) @@ -385,12 +386,13 @@ function DiffEqBase.DiscreteProblem( acs = remove_choose(acs) attrs, transitions, wrap_fun = compile_attrs(acs) - state = ReactiveDynamicsState( + state = ReactiveNetwork( acs, attrs, transitions, wrap_fun, keywords[:tspan][1]; + name = "rn_state", keywords..., ) init_u!(state) @@ -432,26 +434,4 @@ function fetch_params(acs::ReactionNetwork) acs[i, :prmName] => acs[i, :prmVal] for i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P)) )) -end - -# EnsembleProblem's prob_func: sample initial values -function get_prob_func(prob) - vars = prob.p[:__state__][:, :specInitUncertainty] - - prob_func = function (prob, _, _) - prob.p[:__state__] = deepcopy(prob.p[:__state0__]) - for i in eachindex(prob.u0) - rv = randn() * vars[i] - prob.u0[i] = if (sign(rv + prob.u0[i]) == sign(prob.u0[i])) - rv + prob.u0[i] - else - prob.u0[i] - end - end - sync!(prob.p[:__state__], prob.u0, prob.p) - - return prob - end - - return prob_func -end +end \ No newline at end of file diff --git a/src/state.jl b/src/state.jl index 9567317..f739b4b 100644 --- a/src/state.jl +++ b/src/state.jl @@ -1,5 +1,7 @@ using DiffEqBase: NullParameters +using AlgebraicAgents + struct UnfoldedReactant index::Int species::Symbol @@ -10,7 +12,7 @@ end """ Ongoing transition auxiliary structure. """ -mutable struct Transition +@aagent struct Transition trans::Dict{Symbol,Any} t::Float64 @@ -20,7 +22,7 @@ end Base.getindex(state::Transition, key) = state.trans[key] -mutable struct Observable +@aagent struct Observable last::Float64 # last sampling time range::Vector{Union{Tuple{Float64,SampleableValues},SampleableValues}} every::Float64 @@ -29,7 +31,7 @@ mutable struct Observable sampled::Any end -mutable struct ReactiveDynamicsState +@aagent struct ReactiveNetwork acs::ReactionNetwork attrs::Dict{Symbol,Vector} @@ -49,65 +51,67 @@ mutable struct ReactiveDynamicsState wrap_fun::Any history_u::Vector{Vector{Float64}} history_t::Vector{Float64} +end - function ReactiveDynamicsState( - acs::ReactionNetwork, +function ReactiveNetwork( + acs::ReactionNetwork, + attrs, + transition_recipes, + wrap_fun, + t0 = 0; + name = "rn_state", + kwargs..., +) + ongoing_transitions = Transition[] + log = NamedTuple[] + observables = compile_observables(acs) + transitions_attrs = + setdiff( + filter(a -> contains(string(a), "trans"), propertynames(acs.subparts)), + (:trans,), + ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] + transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) + + return ReactiveNetwork( + name, + acs, attrs, transition_recipes, + zeros(nparts(acs, :S)), + fetch_params(acs), + t0, + transitions, + ongoing_transitions, + log, + observables, + kwargs, wrap_fun, - t0 = 0; - kwargs..., + Vector{Float64}[], + Float64[], ) - ongoing_transitions = Transition[] - log = NamedTuple[] - observables = compile_observables(acs) - transitions_attrs = - setdiff( - filter(a -> contains(string(a), "trans"), propertynames(acs.subparts)), - (:trans,), - ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] - transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) - - return new( - acs, - attrs, - transition_recipes, - zeros(nparts(acs, :S)), - fetch_params(acs), - t0, - transitions, - ongoing_transitions, - log, - observables, - kwargs, - wrap_fun, - Vector{Float64}[], - Float64[], - ) - end end # get value of a numeric expression # evaluate compiled numeric expression in context of (u, p, t) -function context_eval(state::ReactiveDynamicsState, o) +function context_eval(state::ReactiveNetwork, o) o = o isa Function ? Base.invokelatest(o, state) : o return o isa Sampleable ? rand(o) : o end -function Base.getindex(state::ReactiveDynamicsState, keys...) +function Base.getindex(state::ReactiveNetwork, keys...) return context_eval( state, (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]], ) end -function init_u!(state::ReactiveDynamicsState) +function init_u!(state::ReactiveNetwork) return (u = fill(0.0, nparts(state, :S)); foreach(i -> u[i] = state[i, :specInitVal], 1:nparts(state, :S)); state.u = u) end -function save!(state::ReactiveDynamicsState) +function save!(state::ReactiveNetwork) return (push!(state.history_u, state.u); push!(state.history_t, state.t)) end @@ -127,7 +131,7 @@ function compile_observables(acs::ReactionNetwork) opts.range, ) - push!(observables, name => Observable(-Inf, range, opts.every, on, missing)) + push!(observables, name => Observable(name, -Inf, range, opts.every, on, missing)) end return observables @@ -149,16 +153,16 @@ function sample_range(rng, state) return r isa Sampleable ? rand(r) : r end -function resample!(state::ReactiveDynamicsState, o::Observable) +function resample!(state::ReactiveNetwork, o::Observable) o.last = state.t isempty(o.range) && (return o.val = missing) return o.sampled = context_eval(state, sample_range(o.range, state)) end -resample(state::ReactiveDynamicsState, o::Symbol) = resample!(state, state.observables[o]) +resample(state::ReactiveNetwork, o::Symbol) = resample!(state, state.observables[o]) -function update_observables(state::ReactiveDynamicsState) +function update_observables(state::ReactiveNetwork) return foreach( o -> (state.t - o.last) >= o.every && resample!(state, o), values(state.observables), @@ -184,11 +188,11 @@ function prune_r_line(r_line) end end -function find_index(species::Symbol, state::ReactiveDynamicsState) +function find_index(species::Symbol, state::ReactiveNetwork) return findfirst(i -> state[i, :specName] == species, 1:nparts(state, :S)) end -function sample_transitions!(state::ReactiveDynamicsState) +function sample_transitions!(state::ReactiveNetwork) for (_, v) in state.transitions empty!(v) end @@ -228,46 +232,46 @@ function sample_transitions!(state::ReactiveDynamicsState) end ## sync -update_u!(state::ReactiveDynamicsState, u) = (state.u .= u) -update_t!(state::ReactiveDynamicsState, t) = (state.t = t) -sync_p!(p, state::ReactiveDynamicsState) = merge!(p, state.p) +update_u!(state::ReactiveNetwork, u) = (state.u .= u) +update_t!(state::ReactiveNetwork, t) = (state.t = t) +sync_p!(p, state::ReactiveNetwork) = merge!(p, state.p) -function sync!(state::ReactiveDynamicsState, u, p) +function sync!(state::ReactiveNetwork, u, p) state.u .= u for k in keys(state.p) haskey(p, k) && (state.p[k] = p[k]) end end -function as_state(u, t, state::ReactiveDynamicsState) +function as_state(u, t, state::ReactiveNetwork) return (state = deepcopy(state); state.u .= u; state.t = t; state) end -function Catlab.CategoricalAlgebra.nparts(state::ReactiveDynamicsState, obj::Symbol) +function Catlab.CategoricalAlgebra.nparts(state::ReactiveNetwork, obj::Symbol) return obj == :T ? length(state.transitions[:transLHS]) : nparts(state.acs, obj) end ## query the state -t(state::ReactiveDynamicsState) = state.t -solverarg(state::ReactiveDynamicsState, arg) = state.solverargs[arg] -take(state::ReactiveDynamicsState, pcs::Symbol) = state.observables[pcs].sampled -log(state::ReactiveDynamicsState, msg) = (println(msg); push!(state.log, (:log, msg))) -state(state::ReactiveDynamicsState) = state +t(state::ReactiveNetwork) = state.t +solverarg(state::ReactiveNetwork, arg) = state.solverargs[arg] +take(state::ReactiveNetwork, pcs::Symbol) = state.observables[pcs].sampled +log(state::ReactiveNetwork, msg) = (println(msg); push!(state.log, (:log, msg))) +state(state::ReactiveNetwork) = state -function periodic(state::ReactiveDynamicsState, period) +function periodic(state::ReactiveNetwork, period) return period == 0.0 || ( length(state.history_t) > 1 && (fld(state.t, period) - fld(state.history_t[end-1], period) > 0) ) end -set_params(state::ReactiveDynamicsState, vals...) = +set_params(state::ReactiveNetwork, vals...) = for (p, v) in vals state.p[p] = v end -function add_to_spawn!(state::ReactiveDynamicsState, hash, n) +function add_to_spawn!(state::ReactiveNetwork, hash, n) ix = findfirst(ix -> state.transition_recipes[:transHash][ix] == hash) return !isnothing(ix) && (state.transition_recipes[:transHash][ix] += n) end diff --git a/test/tutorial_tests.jl b/test/tutorial_tests.jl index 8ff8b8f..03e4215 100644 --- a/test/tutorial_tests.jl +++ b/test/tutorial_tests.jl @@ -3,6 +3,6 @@ using ReactiveDynamics @safeinclude "example" "../tutorial/example.jl" @safeinclude "joins" "../tutorial/joins/joins.jl" @safeinclude "loadsave" "../tutorial/loadsave/loadsave.jl" -@safeinclude "optimize" "../tutorial/optimize/optimize.jl" -@safeinclude "solution wrap" "../tutorial/optimize/optimize_custom.jl" +# @safeinclude "optimize" "../tutorial/optimize/optimize.jl" +# @safeinclude "solution wrap" "../tutorial/optimize/optimize_custom.jl" @safeinclude "toy pharma model" "../tutorial/toy_pharma_model.jl" diff --git a/tutorial/basics.jl b/tutorial/basics.jl index 3b0267e..5a09462 100644 --- a/tutorial/basics.jl +++ b/tutorial/basics.jl @@ -16,7 +16,7 @@ prob = @problematize acs # sol = ReactiveDynamics.solve(prob) -sol = @solve prob trajectories = 20 +sol = @solve prob using Plots From 7b527ce5085dea2edd4e437ccb9a4b0e88d348d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Fri, 8 Sep 2023 15:47:10 +0200 Subject: [PATCH 02/11] simulation under `ReactiveNetwork` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bíma, Jan --- src/solvers.jl | 155 +++++++++++++++++++++++++------------------------ src/state.jl | 41 +------------ 2 files changed, 82 insertions(+), 114 deletions(-) diff --git a/src/solvers.jl b/src/solvers.jl index ddddb86..a36841c 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -298,53 +298,6 @@ function free_blocked_species!(state) end end -""" -Transform an `acs` to a `DiscreteProblem` instance, compatible with standard solvers. - -# Examples - -```julia -transform(DiscreteProblem, acs; schedule = schedule_weighted!) -``` -""" -function transform( - ::Type{DiffEqBase.DiscreteProblem}, - state::ReactiveNetwork; - kwargs..., -) - f = function (du, u, p, t) - state = p[:__state__] - free_blocked_species!(state) - du .= state.u - update_observables(state) - sample_transitions!(state) - evolve!(du, state) - finish!(du, state) - update_u!(state, du) - event_action!(state) - - du .= state.u - push!( - state.log, - (:valuation, t, du' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), - ) - - t = (state.t += state.solverargs[:tstep]) - update_u!(state, du) - save!(state) - sync_p!(p, state) - - return du - end - - return DiffEqBase.DiscreteProblem( - f, - state.u, - (0.0, 2.0), - Dict(state.p..., :__state__ => state, :__state0__ => deepcopy(state)); - kwargs..., - ) -end ## resolve tspan, tstep @@ -358,21 +311,11 @@ function get_tcontrol(tspan, args) return ((0.0, tspan), tstep) end -""" -Transform an `acs` to a `DiscreteProblem` instance, compatible with standard solvers. - -Optionally accepts initial values and parameters, which take precedence over specifications in `acs`. - -# Examples - -```julia -DiscreteProblem(acs, u0, p; tspan = (0.0, 100.0), schedule = schedule_weighted!) -``` -""" -function DiffEqBase.DiscreteProblem( +function ReactiveNetwork( acs::ReactionNetwork, u0 = Dict(), p = DiffEqBase.NullParameters(); + name = "reactive_network", kwargs..., ) assign_defaults!(acs) @@ -386,30 +329,20 @@ function DiffEqBase.DiscreteProblem( acs = remove_choose(acs) attrs, transitions, wrap_fun = compile_attrs(acs) - state = ReactiveNetwork( - acs, - attrs, - transitions, - wrap_fun, - keywords[:tspan][1]; - name = "rn_state", - keywords..., - ) + init_u!(state) save!(state) - prob = transform(DiffEqBase.DiscreteProblem, state; kwargs...) + u0_init = zeros(nparts(state, :S)) u0 isa Dict && foreach( i -> - prob.u0[i] = - if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName]) - u0[acs[i, :specName]] - else - prob.u0[i] - end, + if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName]) + u0_init[i] = u0[acs[i, :specName]] + end, 1:nparts(state, :S), ) + p_ = p == DiffEqBase.NullParameters() ? Dict() : Dict(k => v for (k, v) in p) prob = remake( prob; @@ -426,7 +359,77 @@ function DiffEqBase.DiscreteProblem( ), ) - return prob + ongoing_transitions = Transition[] + log = NamedTuple[] + observables = compile_observables(acs) + transitions_attrs = + setdiff( + filter(a -> contains(string(a), "trans"), propertynames(acs.subparts)), + (:trans,), + ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] + transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) + + return ReactiveNetwork( + name, + acs, + attrs, + transition_recipes, + u0_init, + merge( + prob.p, + p_, + Dict( + :tstep => get(keywords, :tstep, 1), + :strategy => get(keywords, :alloc_strategy, :weighted), + ), + ), + t, + keywords[:tspan][1], + keywords[:tspan], + get(keywords, :tstep, 1), + transitions, + ongoing_transitions, + log, + observables, + kwargs, + wrap_fun, + Vector{Float64}[], + Float64[], + ) +end + +function AlgebraicAgents.step!(state::ReactiveNetwork) + du = copy(state.u) + free_blocked_species!(state) + du .= state.u + update_observables(state) + sample_transitions!(state) + evolve!(du, state) + finish!(du, state) + update_u!(state, du) + event_action!(state) + + du .= state.u + push!( + state.log, + (:valuation, t, du' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), + ) + + t = (state.t += state.solverargs[:tstep]) + update_u!(state, du) + save!(state) + sync_p!(p, state) + + state.u .= du + state.t += state.dt +end + +function AlgebraicAgents._projected_to(state::ReactiveNetwork) + if state.t >= state.tspan[2] + true + else + state.t + end end function fetch_params(acs::ReactionNetwork) diff --git a/src/state.jl b/src/state.jl index f739b4b..7d0c017 100644 --- a/src/state.jl +++ b/src/state.jl @@ -41,6 +41,9 @@ end p::Any t::Float64 + tspan::Tuple{Float64,Float64} + dt::Float64 + transitions::Dict{Symbol,Vector} ongoing_transitions::Vector{Transition} log::Vector{Tuple} @@ -53,44 +56,6 @@ end history_t::Vector{Float64} end -function ReactiveNetwork( - acs::ReactionNetwork, - attrs, - transition_recipes, - wrap_fun, - t0 = 0; - name = "rn_state", - kwargs..., -) - ongoing_transitions = Transition[] - log = NamedTuple[] - observables = compile_observables(acs) - transitions_attrs = - setdiff( - filter(a -> contains(string(a), "trans"), propertynames(acs.subparts)), - (:trans,), - ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] - transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) - - return ReactiveNetwork( - name, - acs, - attrs, - transition_recipes, - zeros(nparts(acs, :S)), - fetch_params(acs), - t0, - transitions, - ongoing_transitions, - log, - observables, - kwargs, - wrap_fun, - Vector{Float64}[], - Float64[], - ) -end - # get value of a numeric expression # evaluate compiled numeric expression in context of (u, p, t) function context_eval(state::ReactiveNetwork, o) From 212da9c5937828c3b4be42aba78140043d06ec42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Fri, 15 Sep 2023 15:45:39 +0200 Subject: [PATCH 03/11] solve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bíma, Jan --- Project.toml | 13 ---- src/ReactiveDynamics.jl | 1 - src/solvers.jl | 116 ++++++++++++----------------- src/state.jl | 4 +- {src/utils => test}/safeinclude.jl | 0 tutorial/basics.jl | 17 ++++- 6 files changed, 60 insertions(+), 91 deletions(-) rename {src/utils => test}/safeinclude.jl (100%) diff --git a/Project.toml b/Project.toml index 354298d..5385d89 100644 --- a/Project.toml +++ b/Project.toml @@ -9,18 +9,12 @@ Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" -DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -DocumenterMarkdown = "997ab1e6-3595-5248-9280-8efb232c3433" GeneratedExpressions = "84d730a5-1eb9-4187-a799-27dd07f33a14" IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -NLopt = "76087f3c-5699-56af-9a33-bf431cd00edd" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" @@ -32,7 +26,6 @@ StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] CSV = "0.10" @@ -40,18 +33,12 @@ Catlab = "0.14" ComponentArrays = "0.14" Crayons = "4.1" DataFrames = "1.6" -DiffEqBase = "6.128" -DifferentialEquations = "7.9" Distributions = "0.25" -Documenter = "0.27" -DocumenterMarkdown = "0.2" GeneratedExpressions = "0.1" IJulia = "1.24" JLD2 = "0.4" JSON = "0.21" MacroTools = "0.5" -NLopt = "1.0" -OrdinaryDiffEq = "6.55" Plots = "1.39" Pluto = "0.19" PlutoUI = "0.7" diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl index a723d9a..ed1da88 100644 --- a/src/ReactiveDynamics.jl +++ b/src/ReactiveDynamics.jl @@ -3,7 +3,6 @@ module ReactiveDynamics using Catlab, Catlab.CategoricalAlgebra, Catlab.Present using Reexport using MacroTools -using NLopt using ComponentArrays @reexport using GeneratedExpressions diff --git a/src/solvers.jl b/src/solvers.jl index a36841c..601530a 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -1,8 +1,3 @@ -# assortment of SciML-compatible problem solvers - -export DiscreteProblem - -using DiffEqBase, DifferentialEquations using Distributions using Random @@ -37,7 +32,7 @@ function get_reqs_ongoing!(reqs, qs, state) for tok in state.ongoing_transitions[i][:transLHS] in(:rate, tok.modality) && (state.ongoing_transitions[i][:transCycleTime] > 0) && - (reqs[tok.index, i] += qs[i] * tok.stoich * state.solverargs[:tstep]) + (reqs[tok.index, i] += qs[i] * tok.stoich * state.dt) in(:nonblock, tok.modality) && (reqs[tok.index, i] += qs[i] * tok.stoich) end end @@ -124,9 +119,8 @@ end """ Evolve transitions, spawn new transitions. """ -function evolve!(u, state) - update_u!(state, u) - actual_allocs = zero(u) +function evolve!(state) + actual_allocs = zero(state.u) ## schedule new transitions reqs = zeros(nparts(state, :S), nparts(state, :T)) @@ -137,8 +131,9 @@ function evolve!(u, state) 1:nparts(state, :T), ) qs .= ceil.(Ref(Int), qs) + @show qs for i = 1:nparts(state, :T) - new_instances = state.solverargs[:tstep] * qs[i] + state[i, :transToSpawn] + new_instances = state.dt * qs[i] + state[i, :transToSpawn] capacity = state[i, :transCapacity] - count(t -> t[:transHash] == state[i, :transHash], state.ongoing_transitions) @@ -148,10 +143,13 @@ function evolve!(u, state) end reqs = get_reqs_init!(reqs, qs, state) + @show reqs allocs = - get_allocs!(reqs, u, state, state[:, :transPriority], state.solverargs[:strategy]) + get_allocs!(reqs, state.u, state, state[:, :transPriority], state.p[:strategy]) + @show allocs qs .= get_init_satisfied(allocs, qs, state) - + @show qs + println("====") push!( state.log, ( @@ -160,7 +158,7 @@ function evolve!(u, state) [(hash, q) for (hash, q) in zip(state[:, :transHash], qs)]..., ), ) - u .-= sum(allocs; dims = 2) + state.u .-= sum(allocs; dims = 2) actual_allocs .+= sum(allocs; dims = 2) # add spawned transitions to the heap @@ -171,7 +169,6 @@ function evolve!(u, state) ) end - update_u!(state, u) ## evolve ongoing transitions reqs = zeros(nparts(state, :S), length(state.ongoing_transitions)) qs = map(t -> t.q, state.ongoing_transitions) @@ -179,10 +176,10 @@ function evolve!(u, state) get_reqs_ongoing!(reqs, qs, state) allocs = get_allocs!( reqs, - u, + state.u, state, map(t -> t[:transPriority], state.ongoing_transitions), - state.solverargs[:strategy], + state.p[:strategy], ) qs .= get_frac_satisfied(allocs, reqs, state) push!( @@ -196,11 +193,11 @@ function evolve!(u, state) ]..., ), ) - u .-= sum(allocs; dims = 2) + state.u .-= sum(allocs; dims = 2) actual_allocs .+= sum(allocs; dims = 2) foreach( - i -> state.ongoing_transitions[i].state += qs[i] * state.solverargs[:tstep], + i -> state.ongoing_transitions[i].state += qs[i] * state.dt, eachindex(state.ongoing_transitions), ) @@ -229,8 +226,7 @@ function event_action!(state) end # collect terminated transitions -function finish!(u, state) - update_u!(state, u) +function finish!(state) val_reward = 0 terminated_all = Dict{Symbol,Float64}() terminated_success = Dict{Symbol,Float64}() @@ -256,7 +252,7 @@ function finish!(u, state) end for tok in trans_[:transLHS] in(:conserved, tok.modality) && ( - u[tok.index] += + state.u[tok.index] += trans_.q * tok.stoich * (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1) @@ -268,12 +264,11 @@ function finish!(u, state) 0 end foreach( - tok -> (u[tok.index] += q * tok.stoich; + tok -> (state.u[tok.index] += q * tok.stoich; val_reward += state[tok.index, :specReward] * q * tok.stoich), toks_rhs, ) - update_u!(state, u) context_eval(state, trans_.trans[:transPostAction]) terminated_all[trans_[:transHash]] = get(terminated_all, trans_[:transHash], 0) + trans_.q @@ -289,7 +284,7 @@ function finish!(u, state) push!(state.log, (:terminated_success, state.t, terminated_success...)) push!(state.log, (:valuation_reward, state.t, val_reward)) - return u + return state.u end function free_blocked_species!(state) @@ -306,15 +301,15 @@ function get_tcontrol(tspan, args) tunit = get(args, :tunit, oneunit(tspan)) tspan = tspan / tunit - tstep = get(args, :tstep, haskey(args, :tstops) ? tspan / args[:tstops] : tunit) / tunit + dt = get(args, :dt, haskey(args, :tstops) ? tspan / args[:tstops] : tunit) / tunit - return ((0.0, tspan), tstep) + return ((0.0, tspan), dt) end function ReactiveNetwork( acs::ReactionNetwork, u0 = Dict(), - p = DiffEqBase.NullParameters(); + p = Dict(); name = "reactive_network", kwargs..., ) @@ -325,39 +320,21 @@ function ReactiveNetwork( ]) merge!(keywords, Dict(collect(kwargs))) merge!(keywords, Dict(:strategy => get(keywords, :alloc_strategy, :weighted))) + keywords[:tspan], keywords[:tstep] = get_tcontrol(keywords[:tspan], keywords) acs = remove_choose(acs) attrs, transitions, wrap_fun = compile_attrs(acs) - - init_u!(state) - save!(state) + transition_recipes = transitions + u0_init = zeros(nparts(acs, :S)) - u0_init = zeros(nparts(state, :S)) - - u0 isa Dict && foreach( - i -> - if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName]) - u0_init[i] = u0[acs[i, :specName]] - end, - 1:nparts(state, :S), - ) - - p_ = p == DiffEqBase.NullParameters() ? Dict() : Dict(k => v for (k, v) in p) - prob = remake( - prob; - u0 = prob.u0, - tspan = keywords[:tspan], - dt = get(keywords, :tstep, 1), - p = merge( - prob.p, - p_, - Dict( - :tstep => get(keywords, :tstep, 1), - :strategy => get(keywords, :alloc_strategy, :weighted), - ), - ), - ) + for i in 1:nparts(acs, :S) + if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName]) + u0_init[i] = u0[acs[i, :specName]] + else + u0_init[i] = acs[i, :specInitVal] + end + end ongoing_transitions = Transition[] log = NamedTuple[] @@ -369,21 +346,19 @@ function ReactiveNetwork( ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) - return ReactiveNetwork( + network = ReactiveNetwork( name, acs, attrs, transition_recipes, u0_init, merge( - prob.p, - p_, + p, Dict( :tstep => get(keywords, :tstep, 1), :strategy => get(keywords, :alloc_strategy, :weighted), ), ), - t, keywords[:tspan][1], keywords[:tspan], get(keywords, :tstep, 1), @@ -396,31 +371,32 @@ function ReactiveNetwork( Vector{Float64}[], Float64[], ) + + save!(network) + + return network end function AlgebraicAgents.step!(state::ReactiveNetwork) - du = copy(state.u) + #du = copy(state.u) free_blocked_species!(state) - du .= state.u + #du .= state.u update_observables(state) sample_transitions!(state) - evolve!(du, state) - finish!(du, state) - update_u!(state, du) + evolve!(state) + finish!(state) + #update_u!(state, u) event_action!(state) - du .= state.u push!( state.log, - (:valuation, t, du' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), + (:valuation, state.t, state.u' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), ) - t = (state.t += state.solverargs[:tstep]) - update_u!(state, du) + #update_u!(state, du) save!(state) - sync_p!(p, state) - state.u .= du + #state.u .= du state.t += state.dt end diff --git a/src/state.jl b/src/state.jl index 7d0c017..2236f51 100644 --- a/src/state.jl +++ b/src/state.jl @@ -1,5 +1,3 @@ -using DiffEqBase: NullParameters - using AlgebraicAgents struct UnfoldedReactant @@ -77,7 +75,7 @@ function init_u!(state::ReactiveNetwork) state.u = u) end function save!(state::ReactiveNetwork) - return (push!(state.history_u, state.u); push!(state.history_t, state.t)) + return (push!(state.history_u, copy(state.u)); push!(state.history_t, state.t)) end function compile_observables(acs::ReactionNetwork) diff --git a/src/utils/safeinclude.jl b/test/safeinclude.jl similarity index 100% rename from src/utils/safeinclude.jl rename to test/safeinclude.jl diff --git a/tutorial/basics.jl b/tutorial/basics.jl index 5a09462..1fda02f 100644 --- a/tutorial/basics.jl +++ b/tutorial/basics.jl @@ -1,23 +1,32 @@ -using ReactiveDynamics +using ReactiveDynamics, AlgebraicAgents # acs = @ReactionNetwork begin # 1.0, X ⟺ Y # end acs = @ReactionNetwork begin - 1.0, X ⟺ Y, name => "transition1" + 1.0, X --> Y, name => "transition1" end @prob_init acs X = 10 Y = 20 @prob_params acs @prob_meta acs tspan = 250 dt = 0.1 -prob = @problematize acs # sol = ReactiveDynamics.solve(prob) -sol = @solve prob +#sol = @solve prob + +prob = @agentize acs +for i in 1:30 + AlgebraicAgents.step!(prob) +end +prob.history_u using Plots @plot sol plot_type = summary +∪ + +prob = @problematize acs +@solve prob From 00e4f6a4747a16dcc11f84e7a13c48de6bfa4b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Sat, 16 Sep 2023 03:49:46 +0200 Subject: [PATCH 04/11] fixes initialization, stepping function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix support for initial values specified in acs - fix support for custom tsteps - fix req/alloc Signed-off-by: Bíma, Jan --- src/interface/create.jl | 2 +- src/interface/update.jl | 2 +- src/solvers.jl | 16 +++++++--------- src/state.jl | 2 +- tutorial/basics.jl | 5 ++++- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/interface/create.jl b/src/interface/create.jl index 1a82374..fe23701 100644 --- a/src/interface/create.jl +++ b/src/interface/create.jl @@ -147,7 +147,7 @@ end function expand_rate(rate) rate = if !(isexpr(rate, :macrocall) && (macroname(rate) == :per_step)) - :(rand(Poisson(max($rate, 0)))) + :(rand(Poisson(max(state.dt * $rate, 0)))) else rate.args[3] end diff --git a/src/interface/update.jl b/src/interface/update.jl index 326c3f6..ce4e811 100644 --- a/src/interface/update.jl +++ b/src/interface/update.jl @@ -489,7 +489,7 @@ Add a jump process (with specified Poisson intensity per unit time step) to a mo macro jump(acsex, inex, acex) return push_to_acs!( acsex, - Expr(:&&, Expr(:call, :rand, :(Poisson(max(@solverarg(:tstep) * $inex, 0)))), acex), + Expr(:&&, Expr(:call, :rand, :(Poisson(max(state.dt * $inex, 0)))), acex), ) end diff --git a/src/solvers.jl b/src/solvers.jl index 601530a..27cefcf 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -107,9 +107,11 @@ function get_init_satisfied(allocs, qs, state) (reqs[tok.index, i] += tok.stoich) end end + @show 2 reqs for i in eachindex(allocs) allocs[i] = reqs[i] == 0.0 ? Inf : floor(allocs[i] / reqs[i]) end + @show allocs foreach(i -> qs[i] = min(qs[i], minimum(allocs[:, i])), 1:size(reqs, 2)) foreach(i -> allocs[:, i] .= reqs[:, i] * qs[i], 1:size(reqs, 2)) @@ -133,7 +135,7 @@ function evolve!(state) qs .= ceil.(Ref(Int), qs) @show qs for i = 1:nparts(state, :T) - new_instances = state.dt * qs[i] + state[i, :transToSpawn] + new_instances = qs[i] + state[i, :transToSpawn] capacity = state[i, :transCapacity] - count(t -> t[:transHash] == state[i, :transHash], state.ongoing_transitions) @@ -165,7 +167,7 @@ function evolve!(state) for i = 1:nparts(state, :T) qs[i] != 0 && push!( state.ongoing_transitions, - Transition(state[i, :transName] * "_@$(state.t)", get_sampled_transition(state, i), state.t, qs[i], 0.0), + Transition(string(state[i, :transName]) * "_@$(state.t)", get_sampled_transition(state, i), state.t, qs[i], 0.0), ) end @@ -258,11 +260,13 @@ function finish!(state) (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1) ) end + q = if trans_.state >= trans_[:transCycleTime] rand(Distributions.Binomial(Int(trans_.q), trans_[:transProbOfSuccess])) else 0 end + foreach( tok -> (state.u[tok.index] += q * tok.stoich; val_reward += state[tok.index, :specReward] * q * tok.stoich), @@ -378,14 +382,11 @@ function ReactiveNetwork( end function AlgebraicAgents.step!(state::ReactiveNetwork) - #du = copy(state.u) free_blocked_species!(state) - #du .= state.u update_observables(state) sample_transitions!(state) evolve!(state) finish!(state) - #update_u!(state, u) event_action!(state) push!( @@ -393,11 +394,8 @@ function AlgebraicAgents.step!(state::ReactiveNetwork) (:valuation, state.t, state.u' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), ) - #update_u!(state, du) save!(state) - - #state.u .= du - state.t += state.dt + return state.t += state.dt end function AlgebraicAgents._projected_to(state::ReactiveNetwork) diff --git a/src/state.jl b/src/state.jl index 2236f51..d70d3bf 100644 --- a/src/state.jl +++ b/src/state.jl @@ -217,7 +217,7 @@ end ## query the state t(state::ReactiveNetwork) = state.t -solverarg(state::ReactiveNetwork, arg) = state.solverargs[arg] +solverarg(state::ReactiveNetwork, arg) = state.p[arg] take(state::ReactiveNetwork, pcs::Symbol) = state.observables[pcs].sampled log(state::ReactiveNetwork, msg) = (println(msg); push!(state.log, (:log, msg))) state(state::ReactiveNetwork) = state diff --git a/tutorial/basics.jl b/tutorial/basics.jl index 1fda02f..e05f7f7 100644 --- a/tutorial/basics.jl +++ b/tutorial/basics.jl @@ -10,7 +10,7 @@ end @prob_init acs X = 10 Y = 20 @prob_params acs -@prob_meta acs tspan = 250 dt = 0.1 +@prob_meta acs tspan = 250 dt = 0.11 # sol = ReactiveDynamics.solve(prob) @@ -23,6 +23,9 @@ for i in 1:30 AlgebraicAgents.step!(prob) end prob.history_u + + + using Plots @plot sol plot_type = summary From 4e93e19e0ccb8b9a14d6cbc7d7c63f8c0fd9494c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Sat, 16 Sep 2023 23:09:22 +0200 Subject: [PATCH 05/11] fix plotting, solution, log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bíma, Jan --- src/ReactiveDynamics.jl | 16 +- src/compilers.jl | 8 +- src/interface/create.jl | 14 +- src/interface/plots.jl | 19 +++ src/interface/reaction_parser.jl | 2 +- src/interface/solve.jl | 50 +------ src/loadsave.jl | 6 +- src/operators/equalize.jl | 2 +- src/operators/joins.jl | 4 +- src/optim.jl | 244 ------------------------------- src/solvers.jl | 53 ++++--- src/state.jl | 67 ++++----- tutorial/basics.jl | 36 ++--- 13 files changed, 113 insertions(+), 408 deletions(-) create mode 100644 src/interface/plots.jl delete mode 100644 src/optim.jl diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl index ed1da88..b0eef6f 100644 --- a/src/ReactiveDynamics.jl +++ b/src/ReactiveDynamics.jl @@ -74,7 +74,7 @@ end @acset_type FoldedReactionNetworkType(TheoryReactionNetwork) -const ReactionNetwork = FoldedReactionNetworkType{ +const ReactionNetworkSchema = FoldedReactionNetworkType{ Symbol, Union{String,Symbol,Missing}, SampleableValues, @@ -131,11 +131,11 @@ defargs = Dict( ) compilable_attrs = - filter(attr -> eltype(attr) == SampleableValues, propertynames(ReactionNetwork())) + filter(attr -> eltype(attr) == SampleableValues, propertynames(ReactionNetworkSchema())) species_modalities = [:nonblock, :conserved, :rate] -function assign_defaults!(acs::ReactionNetwork) +function assign_defaults!(acs::ReactionNetworkSchema) for (_, v_) in defargs, (k, v) in v_ for i = 1:length(subpart(acs, k)) isnothing(acs[i, k]) && (subpart(acs, k)[i] = v) @@ -160,12 +160,12 @@ function assign_defaults!(acs::ReactionNetwork) return acs end -function ReactionNetwork(transitions, reactants, obs, events) - return merge_acs!(ReactionNetwork(), transitions, reactants, obs, events) +function ReactionNetworkSchema(transitions, reactants, obs, events) + return merge_acs!(ReactionNetworkSchema(), transitions, reactants, obs, events) end -function ReactionNetwork(transitions, reactants, obs) - return merge_acs!(ReactionNetwork(), transitions, reactants, obs, []) +function ReactionNetworkSchema(transitions, reactants, obs) + return merge_acs!(ReactionNetworkSchema(), transitions, reactants, obs, []) end function add_obs!(acs, obs) @@ -194,7 +194,7 @@ function add_obs!(acs, obs) return acs end -function merge_acs!(acs::ReactionNetwork, transitions, reactants, obs, events) +function merge_acs!(acs::ReactionNetworkSchema, transitions, reactants, obs, events) foreach( t -> add_part!(acs, :T; trans = t[1][2], transRate = t[1][1], t[2]...), transitions, diff --git a/src/compilers.jl b/src/compilers.jl index 4588b69..9edccc6 100644 --- a/src/compilers.jl +++ b/src/compilers.jl @@ -112,12 +112,12 @@ function wrap_expr(fex, species_names, prm_names, varmap) ) push!(letex.args[2].args, fex) - # the function shall be a function of the dynamic ReactiveNetwork structure: letex -> :(state -> $letex) + # the function shall be a function of the dynamic ReactionNetworkSchema structure: letex -> :(state -> $letex) # eval the expression to a Julia function, save that function into the "compiled" acset return eval(:(state -> $letex)) end -function get_wrap_fun(acs::ReactionNetwork) +function get_wrap_fun(acs::ReactionNetworkSchema) species_names = collect(acs[:, :specName]) prm_names = collect(acs[:, :prmName]) varmap = Dict([name => :(state.u[$i]) for (i, name) in enumerate(species_names)]) @@ -133,7 +133,7 @@ function skip_compile(attr) (string(attr) == "trans") end -function compile_attrs(acs::ReactionNetwork) +function compile_attrs(acs::ReactionNetworkSchema) species_names = collect(acs[:, :specName]) prm_names = collect(acs[:, :prmName]) varmap = Dict([name => :(state.u[$i]) for (i, name) in enumerate(species_names)]) @@ -169,7 +169,7 @@ function compile_attrs(acs::ReactionNetwork) return attrs, transitions, wrap_fun end -function remove_choose(acs::ReactionNetwork) +function remove_choose(acs::ReactionNetworkSchema) acs = deepcopy(acs) pcs = [] for attr in propertynames(acs.subparts) diff --git a/src/interface/create.jl b/src/interface/create.jl index fe23701..ec49f85 100644 --- a/src/interface/create.jl +++ b/src/interface/create.jl @@ -1,6 +1,6 @@ # reaction network DSL: CREATE part; reaction line and event parsing -export @ReactionNetwork +export @ReactionNetworkSchema using MacroTools: prewalk, postwalk, striplines, isexpr using Symbolics: build_function, get_variables @@ -45,7 +45,7 @@ Custom functions and sampleable objects can be used as numeric parameters. Note # Examples ```julia -acs = @ReactionNetwork begin +acs = @ReactionNetworkSchema begin 1.0, X ⟶ Y 1.0, X ⟶ Y, priority => 6.0, prob => 0.7, capacity => 3.0 1.0, ∅ --> (Poisson(0.3γ)X, Poisson(0.5)Y) @@ -57,17 +57,17 @@ end @solve_and_plot acs ``` """ -macro ReactionNetwork end +macro ReactionNetworkSchema end -macro ReactionNetwork() +macro ReactionNetworkSchema() return make_ReactionNetwork(:()) end -macro ReactionNetwork(ex) +macro ReactionNetworkSchema(ex) return make_ReactionNetwork(ex; eval_module = __module__) end -macro ReactionNetwork(ex, args...) +macro ReactionNetworkSchema(ex, args...) return make_ReactionNetwork( generate(Expr(:braces, ex, args...); eval_module = __module__); eval_module = __module__, @@ -78,7 +78,7 @@ function make_ReactionNetwork(ex::Expr; eval_module = @__MODULE__) blockex = generate(ex; eval_module) blockex = unblock_shallow!(blockex) - return :(ReactionNetwork(get_data($(QuoteNode(blockex)))...)) + return :(ReactionNetworkSchema(get_data($(QuoteNode(blockex)))...)) end ### Functions that process the input and rephrase it as a reaction system ### diff --git a/src/interface/plots.jl b/src/interface/plots.jl new file mode 100644 index 0000000..22de29f --- /dev/null +++ b/src/interface/plots.jl @@ -0,0 +1,19 @@ +using Plots + +function plot_df(df::DataFrames.DataFrame, t_ix = 1) + data = Matrix(df) + t = @view data[:, t_ix] + data_ = @view data[:, setdiff(1:size(data, 2), (t_ix,))] + colnames = reshape(DataFrames.names(df)[setdiff(1:size(data, 2), (t_ix,))], 1, :) + + Plots.plot(t, data_, labels = colnames, xlabel = "t") +end + +# plot reduction +function AlgebraicAgents._draw(prob::ReactionNetworkProblem, vars = string.(prob.acs[:, :specName]); kwargs...) + p = plot() + for var in vars + p = plot!(p, prob.sol[!, "t"], prob.sol[!, var]; label="$var", xlabel="time", ylabel="quantity", kwargs...) + end + p +end diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl index df69f53..2c566bb 100644 --- a/src/interface/reaction_parser.jl +++ b/src/interface/reaction_parser.jl @@ -29,7 +29,7 @@ function recursively_choose(r_line, state) end end -function extract_reactants(r_line, state::ReactiveNetwork) +function extract_reactants(r_line, state::ReactionNetworkProblem) r_line = recursively_choose(r_line, state) return recursive_find_reactants!( diff --git a/src/interface/solve.jl b/src/interface/solve.jl index 029aee3..e8a1913 100644 --- a/src/interface/solve.jl +++ b/src/interface/solve.jl @@ -1,56 +1,8 @@ -export @agentize, @solve, @plot -export @optimize, @fit, @fit_and_plot, @build_solver +export @agentize import MacroTools import Plots -""" -Convert a model to a `ReactiveNetwork`. If passed a problem instance, return the instance. - -# Examples - -```julia -@agentize acs tspan = 1:100 -``` -""" -macro agentize(acsex, args...) - args, kwargs = args_kwargs(args) - quote - if $(esc(acsex)) isa ReactiveNetwork - $(esc(acsex)) - else - ReactiveNetwork($(esc(acsex)), $(args...); $(kwargs...)) - end - end -end - -""" -Solve the problem. Solverargs passed at the calltime take precedence. - -# Examples - -```julia -@solve prob -@solve prob tspan = 1:100 -@solve prob tspan = 100 -``` -""" -macro solve(probex, args...) - args, kwargs = args_kwargs(args) - mode = find_kwargex_delete!(kwargs, :mode, nothing) - !isnothing(findfirst(el -> el.args[1] == :trajectories, kwargs)) && (mode = :ensemble) - - quote - prob = if $(esc(probex)) isa ReactiveNetwork - $(esc(probex)) - else - ReactiveNetwork($(esc(probex)), $(args...); $(kwargs...)) - end - - simulate(prob) - end -end - # auxiliary plotting functions function plot_summary(s, labels, ixs; kwargs...) isempty(ixs) && return @warn "Set of species to plot must be non-empty!" diff --git a/src/loadsave.jl b/src/loadsave.jl index da3d8b4..89571fb 100644 --- a/src/loadsave.jl +++ b/src/loadsave.jl @@ -16,7 +16,7 @@ const objects_aliases = Dict( :obs => "obs", ) -const RN_attrs = string.(propertynames(ReactionNetwork().subparts)) +const RN_attrs = string.(propertynames(ReactionNetworkSchema().subparts)) function get_attrs(object) object = object isa Symbol ? objects_aliases[object] : object @@ -24,7 +24,7 @@ function get_attrs(object) return filter(x -> occursin(object, x), RN_attrs) end -function export_network(acs::ReactionNetwork) +function export_network(acs::ReactionNetworkSchema) dict = Dict() for (key, val) in objects_aliases push!(dict, val => []) @@ -113,7 +113,7 @@ function import_network(path::AbstractString) end end -function export_network(acs::ReactionNetwork, path::AbstractString) +function export_network(acs::ReactionNetworkSchema, path::AbstractString) if splitext(path)[2] == ".csv" exported_network = export_network(acs) paths = DataFrame(; type = [], path = []) diff --git a/src/operators/equalize.jl b/src/operators/equalize.jl index cbe357d..e679ec9 100644 --- a/src/operators/equalize.jl +++ b/src/operators/equalize.jl @@ -21,7 +21,7 @@ function get_eqs_ff(eq) end end -function equalize!(acs::ReactionNetwork, eqs = []) +function equalize!(acs::ReactionNetworkSchema, eqs = []) specmap = Dict() for block in eqs block_alias = findfirst(e -> e[1] == :alias, block) diff --git a/src/operators/joins.jl b/src/operators/joins.jl index 40ebc73..f5c3276 100644 --- a/src/operators/joins.jl +++ b/src/operators/joins.jl @@ -59,7 +59,7 @@ end """ Prepend species names with a model identifier (unless a global species name). """ -function prepend!(acs::ReactionNetwork, name = gensym("acs"), eqs = []) +function prepend!(acs::ReactionNetworkSchema, name = gensym("acs"), eqs = []) specmap = Dict() for i = 1:nparts(acs, :S) new_name = normalize_name(name, i, acs[i, :specName], eqs) @@ -199,7 +199,7 @@ Model variables / parameter values and metadata are propagated; the last model t macro join(exs...) callex = :( begin - acs_new = ReactionNetwork() + acs_new = ReactionNetworkSchema() end ) exs = collect(exs) diff --git a/src/optim.jl b/src/optim.jl deleted file mode 100644 index a30e179..0000000 --- a/src/optim.jl +++ /dev/null @@ -1,244 +0,0 @@ -function build_parametrized_solver(acs, init_vec, u0, params; trajectories = 1) - prob = DiscreteProblem(acs) - vars = prob.p[:__state__][:, :specInitUncertainty] - init_vec = deepcopy(init_vec) - - function (vec) - vec = vec isa ComponentVector ? vec : (init_vec .= vec) - data = [] - for _ = 1:trajectories - prob.p[:__state__] = deepcopy(prob.p[:__state0__]) - for i in eachindex(prob.u0) - rv = randn() * vars[i] - prob.u0[i] = if (sign(rv + prob.u0[i]) == sign(prob.u0[i])) - rv + prob.u0[i] - else - prob.u0[i] - end - end - - for (i, k) in enumerate(wkeys(u0)) - prob.u0[k] = vec.species[i] - end - for k in wkeys(params) - prob.p[k] = vec[k] - end - - sync!(prob.p[:__state__], prob.u0, prob.p) - push!(data, solve(prob)) - end - - return data - end -end - -function build_parametrized_solver_(acs, init_vec, u0, params; trajectories = 1) - prob = DiscreteProblem(acs) - vars = prob.p[:__state__][:, :specInitUncertainty] - init_vec = deepcopy(init_vec) - - function (vec) - vec = vec isa ComponentVector ? vec : (init_vec .= vec; init_vec) - data = map(1:trajectories) do _ - prob.p[:__state__] = deepcopy(prob.p[:__state0__]) - for i in eachindex(prob.u0) - rv = randn() * vars[i] - prob.u0[i] = if (sign(rv + prob.u0[i]) == sign(prob.u0[i])) - rv + prob.u0[i] - else - prob.u0[i] - end - end - - for (i, k) in enumerate(wkeys(u0)) - prob.u0[k] = vec.species[i] - end - for k in wkeys(params) - prob.p[k] = vec[k] - end - - sync!(prob.p[:__state__], prob.u0, prob.p) - - return solve(prob) - end - - return trajectories == 1 ? data[1] : EnsembleSolution(data, 0.0, true) - end -end - -## optimization part - -BOUND_DEFAULT = 5000 - -function optim!(obj, init; nlopt_kwargs...) - nlopt_kwargs = Dict(nlopt_kwargs) - alg = pop!(nlopt_kwargs, :algorithm, :GN_DIRECT) - - opt = Opt(alg, length(init)) - - # match to a ComponentVector - foreach( - o -> setproperty!(opt, o...), - filter(x -> x[1] in propertynames(opt), nlopt_kwargs), - ) - if get(nlopt_kwargs, :objective, min) == min - (opt.min_objective = obj) - else - (opt.max_objective = obj) - end - - return optimize(opt, deepcopy(init)) -end - -const n_steps = 100 - -# loss objective given an objective expression -function build_loss_objective( - acs, - init_vec, - u0, - params, - obex; - loss = identity, - trajectories = 1, - min_t = -Inf, - max_t = Inf, - final_only = false, -) - ob = eval(get_wrap_fun(acs)(obex)) - obj_ = build_parametrized_solver(acs, init_vec, u0, params; trajectories) - - function (vec, _) - ls = [] - for sol in obj_(vec) - t_points = if final_only - [last(sol.t)] - else - min_t = max(min_t, sol.prob.tspan[1]) - max_t = min(max_t, sol.prob.tspan[2]) - - range(min_t, max_t; length = n_steps) - end - - push!( - ls, - mean(t -> loss(ob(as_state(sol(t), t, sol.prob.p[:__state__]))), t_points), - ) - end - - return mean(ls) - end -end - -# loss objective given empirical data -function build_loss_objective_datapoints( - acs, - init_vec, - u0, - params, - t, - data, - vars; - loss = abs2, - trajectories = 1, -) - obj_ = build_parametrized_solver(acs, init_vec, u0, params; trajectories) - - function (vec, _) - ls = [] - for sol in obj_(vec) - push!( - ls, - mean( - t -> - sum(i -> loss(sol(t[2])[i[2]] - data[i[1], t[1]]), enumerate(vars)), - enumerate(t), - ), - ) - end - - return mean(ls) - end -end - -# set initial model parameter values in an optimization problem -function prep_params!(params, prob) - for (k, v) in params - (v === NaN) && wset!(params, k, get(prob.p, k, NaN)) - end - any(p -> (p[2] === NaN) && @warn("Uninitialized prm: $p"), params) - - return params -end - -# set initial model variable values in an optimization problem -function prep_u0!(u0, prob) - for (k, v) in u0 - (v === NaN) && wset!(u0, k, get(prob.u0, k, NaN)) - end - any(u -> (u[2] === NaN) && @warn("Uninitialized prm: $(u[1])"), u0) - - return u0 -end - -""" -Extract symbolic variables referenced in `acs`, `args`. -""" -function get_free_vars(acs, args) - u0_syms = collect(acs[:, :specName]) - p_syms = collect(acs[:, :prmName]) - u0 = [] - p = [] - - for arg in args - if arg isa Symbol - (k, v) = (arg, NaN) - elseif isexpr(arg, :(=)) - (k, v) = (arg.args[1], arg.args[2]) - else - continue - end - - if ((k in u0_syms || k isa Number) && !in(k, wkeys(u0))) - push!(u0, k => v) - elseif (k in p_syms && !in(k, wkeys(p))) - push!(p, k => v) - end - end - - u0_ = [] - for (k, v) in u0 - if k isa Number - push!(u0_, Int(k) => v) - else - for i = 1:length(subpart(acs, :specName)) - (acs[i, :specName] == k) && (push!(u0_, i => v); break) - end - end - end - - return u0_, p -end - -""" -Resolve symbolic / positional model variable names to positional. -""" -function get_vars(acs, args) - (args == :()) && return args - args_ = [] - - for arg in (MacroTools.isexpr(args, :vect, :tuple) ? args.args : [args]) - arg = recursively_expand_dots(arg) - if arg isa Number - push!(args_, Int(arg)) - else - for i = 1:length(subpart(acs, :specName)) - !isnothing(acs[i, :specName]) && - (acs[i, :specName] == arg) && - (push!(args_, i); break) - end - end - end - - return args_ -end diff --git a/src/solvers.jl b/src/solvers.jl index 27cefcf..2737fe1 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -1,6 +1,8 @@ using Distributions using Random +export ReactionNetworkProblem + function get_sampled_transition(state, i) transition = Dict{Symbol,Any}() foreach(k -> push!(transition, k => state[i, k]), keys(state.transitions)) @@ -107,11 +109,11 @@ function get_init_satisfied(allocs, qs, state) (reqs[tok.index, i] += tok.stoich) end end - @show 2 reqs + for i in eachindex(allocs) allocs[i] = reqs[i] == 0.0 ? Inf : floor(allocs[i] / reqs[i]) end - @show allocs + foreach(i -> qs[i] = min(qs[i], minimum(allocs[:, i])), 1:size(reqs, 2)) foreach(i -> allocs[:, i] .= reqs[:, i] * qs[i], 1:size(reqs, 2)) @@ -133,7 +135,7 @@ function evolve!(state) 1:nparts(state, :T), ) qs .= ceil.(Ref(Int), qs) - @show qs + for i = 1:nparts(state, :T) new_instances = qs[i] + state[i, :transToSpawn] capacity = @@ -145,13 +147,12 @@ function evolve!(state) end reqs = get_reqs_init!(reqs, qs, state) - @show reqs + allocs = get_allocs!(reqs, state.u, state, state[:, :transPriority], state.p[:strategy]) - @show allocs + qs .= get_init_satisfied(allocs, qs, state) - @show qs - println("====") + push!( state.log, ( @@ -260,7 +261,7 @@ function finish!(state) (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1) ) end - + q = if trans_.state >= trans_[:transCycleTime] rand(Distributions.Binomial(Int(trans_.q), trans_[:transProbOfSuccess])) else @@ -297,7 +298,6 @@ function free_blocked_species!(state) end end - ## resolve tspan, tstep function get_tcontrol(tspan, args) @@ -310,8 +310,8 @@ function get_tcontrol(tspan, args) return ((0.0, tspan), dt) end -function ReactiveNetwork( - acs::ReactionNetwork, +function ReactionNetworkProblem( + acs::ReactionNetworkSchema, u0 = Dict(), p = Dict(); name = "reactive_network", @@ -350,7 +350,8 @@ function ReactiveNetwork( ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) - network = ReactiveNetwork( + sol = DataFrame("t" => Float64[], (string(name) => Float64[] for name in acs[:, :specName])...) + network = ReactionNetworkProblem( name, acs, attrs, @@ -359,7 +360,6 @@ function ReactiveNetwork( merge( p, Dict( - :tstep => get(keywords, :tstep, 1), :strategy => get(keywords, :alloc_strategy, :weighted), ), ), @@ -370,10 +370,8 @@ function ReactiveNetwork( ongoing_transitions, log, observables, - kwargs, wrap_fun, - Vector{Float64}[], - Float64[], + sol ) save!(network) @@ -381,7 +379,18 @@ function ReactiveNetwork( return network end -function AlgebraicAgents.step!(state::ReactiveNetwork) +function AlgebraicAgents._reinit!(state::ReactionNetworkProblem) + state.u .= isempty(state.sol) ? state.u : Vector(state.sol[1, 2:end]) + state.t = state.tspan[1] + empty!(state.ongoing_transitions) + empty!(state.log) + state.observables = compile_observables(state.acs) + empty!(state.sol) + + state +end + +function AlgebraicAgents._step!(state::ReactionNetworkProblem) free_blocked_species!(state) update_observables(state) sample_transitions!(state) @@ -398,15 +407,11 @@ function AlgebraicAgents.step!(state::ReactiveNetwork) return state.t += state.dt end -function AlgebraicAgents._projected_to(state::ReactiveNetwork) - if state.t >= state.tspan[2] - true - else - state.t - end +function AlgebraicAgents._projected_to(state::ReactionNetworkProblem) + state.t > state.tspan[2] ? true : state.t end -function fetch_params(acs::ReactionNetwork) +function fetch_params(acs::ReactionNetworkSchema) return Dict{Symbol,Any}(( acs[i, :prmName] => acs[i, :prmVal] for i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P)) diff --git a/src/state.jl b/src/state.jl index d70d3bf..0aba482 100644 --- a/src/state.jl +++ b/src/state.jl @@ -1,4 +1,5 @@ -using AlgebraicAgents +@reexport using AlgebraicAgents +using DataFrames struct UnfoldedReactant index::Int @@ -29,8 +30,8 @@ Base.getindex(state::Transition, key) = state.trans[key] sampled::Any end -@aagent struct ReactiveNetwork - acs::ReactionNetwork +@aagent struct ReactionNetworkProblem + acs::ReactionNetworkSchema attrs::Dict{Symbol,Vector} transition_recipes::Dict{Symbol,Vector} @@ -47,38 +48,36 @@ end log::Vector{Tuple} observables::Dict{Symbol,Observable} - solverargs::Any wrap_fun::Any - history_u::Vector{Vector{Float64}} - history_t::Vector{Float64} + sol::DataFrame end # get value of a numeric expression # evaluate compiled numeric expression in context of (u, p, t) -function context_eval(state::ReactiveNetwork, o) +function context_eval(state::ReactionNetworkProblem, o) o = o isa Function ? Base.invokelatest(o, state) : o return o isa Sampleable ? rand(o) : o end -function Base.getindex(state::ReactiveNetwork, keys...) +function Base.getindex(state::ReactionNetworkProblem, keys...) return context_eval( state, (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]], ) end -function init_u!(state::ReactiveNetwork) +function init_u!(state::ReactionNetworkProblem) return (u = fill(0.0, nparts(state, :S)); foreach(i -> u[i] = state[i, :specInitVal], 1:nparts(state, :S)); state.u = u) end -function save!(state::ReactiveNetwork) - return (push!(state.history_u, copy(state.u)); push!(state.history_t, state.t)) +function save!(state::ReactionNetworkProblem) + return push!(state.sol, (state.t, state.u[:]...)) end -function compile_observables(acs::ReactionNetwork) +function compile_observables(acs::ReactionNetworkSchema) observables = Dict{Symbol,Observable}() species_names = collect(acs[:, :specName]) prm_names = collect(acs[:, :prmName]) @@ -116,16 +115,16 @@ function sample_range(rng, state) return r isa Sampleable ? rand(r) : r end -function resample!(state::ReactiveNetwork, o::Observable) +function resample!(state::ReactionNetworkProblem, o::Observable) o.last = state.t isempty(o.range) && (return o.val = missing) return o.sampled = context_eval(state, sample_range(o.range, state)) end -resample(state::ReactiveNetwork, o::Symbol) = resample!(state, state.observables[o]) +resample(state::ReactionNetworkProblem, o::Symbol) = resample!(state, state.observables[o]) -function update_observables(state::ReactiveNetwork) +function update_observables(state::ReactionNetworkProblem) return foreach( o -> (state.t - o.last) >= o.every && resample!(state, o), values(state.observables), @@ -151,11 +150,11 @@ function prune_r_line(r_line) end end -function find_index(species::Symbol, state::ReactiveNetwork) +function find_index(species::Symbol, state::ReactionNetworkProblem) return findfirst(i -> state[i, :specName] == species, 1:nparts(state, :S)) end -function sample_transitions!(state::ReactiveNetwork) +function sample_transitions!(state::ReactionNetworkProblem) for (_, v) in state.transitions empty!(v) end @@ -194,47 +193,35 @@ function sample_transitions!(state::ReactiveNetwork) end end -## sync -update_u!(state::ReactiveNetwork, u) = (state.u .= u) -update_t!(state::ReactiveNetwork, t) = (state.t = t) -sync_p!(p, state::ReactiveNetwork) = merge!(p, state.p) - -function sync!(state::ReactiveNetwork, u, p) - state.u .= u - for k in keys(state.p) - haskey(p, k) && (state.p[k] = p[k]) - end -end - -function as_state(u, t, state::ReactiveNetwork) +function as_state(u, t, state::ReactionNetworkProblem) return (state = deepcopy(state); state.u .= u; state.t = t; state) end -function Catlab.CategoricalAlgebra.nparts(state::ReactiveNetwork, obj::Symbol) +function Catlab.CategoricalAlgebra.nparts(state::ReactionNetworkProblem, obj::Symbol) return obj == :T ? length(state.transitions[:transLHS]) : nparts(state.acs, obj) end ## query the state -t(state::ReactiveNetwork) = state.t -solverarg(state::ReactiveNetwork, arg) = state.p[arg] -take(state::ReactiveNetwork, pcs::Symbol) = state.observables[pcs].sampled -log(state::ReactiveNetwork, msg) = (println(msg); push!(state.log, (:log, msg))) -state(state::ReactiveNetwork) = state +t(state::ReactionNetworkProblem) = state.t +solverarg(state::ReactionNetworkProblem, arg) = state.p[arg] +take(state::ReactionNetworkProblem, pcs::Symbol) = state.observables[pcs].sampled +log(state::ReactionNetworkProblem, msg) = (println(msg); push!(state.log, (:log, msg))) +state(state::ReactionNetworkProblem) = state -function periodic(state::ReactiveNetwork, period) +function periodic(state::ReactionNetworkProblem, period) return period == 0.0 || ( length(state.history_t) > 1 && (fld(state.t, period) - fld(state.history_t[end-1], period) > 0) ) end -set_params(state::ReactiveNetwork, vals...) = +set_params(state::ReactionNetworkProblem, vals...) = for (p, v) in vals state.p[p] = v end -function add_to_spawn!(state::ReactiveNetwork, hash, n) +function add_to_spawn!(state::ReactionNetworkProblem, hash, n) ix = findfirst(ix -> state.transition_recipes[:transHash][ix] == hash) return !isnothing(ix) && (state.transition_recipes[:transHash][ix] += n) -end +end \ No newline at end of file diff --git a/tutorial/basics.jl b/tutorial/basics.jl index e05f7f7..de13b55 100644 --- a/tutorial/basics.jl +++ b/tutorial/basics.jl @@ -1,35 +1,21 @@ -using ReactiveDynamics, AlgebraicAgents +using ReactiveDynamics -# acs = @ReactionNetwork begin -# 1.0, X ⟺ Y -# end - -acs = @ReactionNetwork begin +# define the network +acs = @ReactionNetworkSchema begin 1.0, X --> Y, name => "transition1" end @prob_init acs X = 10 Y = 20 @prob_params acs -@prob_meta acs tspan = 250 dt = 0.11 - - -# sol = ReactiveDynamics.solve(prob) - -#sol = @solve prob - -prob = @agentize acs - -for i in 1:30 - AlgebraicAgents.step!(prob) -end -prob.history_u - +@prob_meta acs tspan = 25 dt = 0.10 +# convert network into an AlgAgents hierarchy +prob = ReactionNetworkProblem(acs) -using Plots +# simulate +simulate(prob) -@plot sol plot_type = summary -∪ +# access solution +prob.sol -prob = @problematize acs -@solve prob +draw(prob) From aec58202af069e8f1375403fa4c4875f9e9fe88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= Date: Sun, 17 Sep 2023 00:52:30 +0200 Subject: [PATCH 06/11] reformat [skip ci] --- src/interface/plots.jl | 20 ++++++++++++++++---- src/interface/solve.jl | 2 +- src/solvers.jl | 41 ++++++++++++++++++++++++----------------- src/state.jl | 6 ++---- tutorial/basics.jl | 1 + 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/interface/plots.jl b/src/interface/plots.jl index 22de29f..23892e0 100644 --- a/src/interface/plots.jl +++ b/src/interface/plots.jl @@ -6,14 +6,26 @@ function plot_df(df::DataFrames.DataFrame, t_ix = 1) data_ = @view data[:, setdiff(1:size(data, 2), (t_ix,))] colnames = reshape(DataFrames.names(df)[setdiff(1:size(data, 2), (t_ix,))], 1, :) - Plots.plot(t, data_, labels = colnames, xlabel = "t") + return Plots.plot(t, data_; labels = colnames, xlabel = "t") end # plot reduction -function AlgebraicAgents._draw(prob::ReactionNetworkProblem, vars = string.(prob.acs[:, :specName]); kwargs...) +function AlgebraicAgents._draw( + prob::ReactionNetworkProblem, + vars = string.(prob.acs[:, :specName]); + kwargs..., +) p = plot() for var in vars - p = plot!(p, prob.sol[!, "t"], prob.sol[!, var]; label="$var", xlabel="time", ylabel="quantity", kwargs...) + p = plot!( + p, + prob.sol[!, "t"], + prob.sol[!, var]; + label = "$var", + xlabel = "time", + ylabel = "quantity", + kwargs..., + ) end - p + return p end diff --git a/src/interface/solve.jl b/src/interface/solve.jl index e8a1913..e38956c 100644 --- a/src/interface/solve.jl +++ b/src/interface/solve.jl @@ -198,4 +198,4 @@ function plot_from_log(state, record_type, ixs; kwargs...) label = reshape(label, 1, :), kwargs..., ) -end \ No newline at end of file +end diff --git a/src/solvers.jl b/src/solvers.jl index 2737fe1..f53b001 100644 --- a/src/solvers.jl +++ b/src/solvers.jl @@ -148,8 +148,7 @@ function evolve!(state) reqs = get_reqs_init!(reqs, qs, state) - allocs = - get_allocs!(reqs, state.u, state, state[:, :transPriority], state.p[:strategy]) + allocs = get_allocs!(reqs, state.u, state, state[:, :transPriority], state.p[:strategy]) qs .= get_init_satisfied(allocs, qs, state) @@ -168,7 +167,13 @@ function evolve!(state) for i = 1:nparts(state, :T) qs[i] != 0 && push!( state.ongoing_transitions, - Transition(string(state[i, :transName]) * "_@$(state.t)", get_sampled_transition(state, i), state.t, qs[i], 0.0), + Transition( + string(state[i, :transName]) * "_@$(state.t)", + get_sampled_transition(state, i), + state.t, + qs[i], + 0.0, + ), ) end @@ -332,7 +337,7 @@ function ReactionNetworkProblem( transition_recipes = transitions u0_init = zeros(nparts(acs, :S)) - for i in 1:nparts(acs, :S) + for i = 1:nparts(acs, :S) if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName]) u0_init[i] = u0[acs[i, :specName]] else @@ -350,19 +355,17 @@ function ReactionNetworkProblem( ) ∪ [:transLHS, :transRHS, :transToSpawn, :transHash] transitions = Dict{Symbol,Vector}(a => [] for a in transitions_attrs) - sol = DataFrame("t" => Float64[], (string(name) => Float64[] for name in acs[:, :specName])...) + sol = DataFrame( + "t" => Float64[], + (string(name) => Float64[] for name in acs[:, :specName])..., + ) network = ReactionNetworkProblem( name, acs, attrs, transition_recipes, - u0_init, - merge( - p, - Dict( - :strategy => get(keywords, :alloc_strategy, :weighted), - ), - ), + u0_init, + merge(p, Dict(:strategy => get(keywords, :alloc_strategy, :weighted))), keywords[:tspan][1], keywords[:tspan], get(keywords, :tstep, 1), @@ -371,7 +374,7 @@ function ReactionNetworkProblem( log, observables, wrap_fun, - sol + sol, ) save!(network) @@ -387,7 +390,7 @@ function AlgebraicAgents._reinit!(state::ReactionNetworkProblem) state.observables = compile_observables(state.acs) empty!(state.sol) - state + return state end function AlgebraicAgents._step!(state::ReactionNetworkProblem) @@ -400,7 +403,11 @@ function AlgebraicAgents._step!(state::ReactionNetworkProblem) push!( state.log, - (:valuation, state.t, state.u' * [state[i, :specValuation] for i = 1:nparts(state, :S)]), + ( + :valuation, + state.t, + state.u' * [state[i, :specValuation] for i = 1:nparts(state, :S)], + ), ) save!(state) @@ -408,7 +415,7 @@ function AlgebraicAgents._step!(state::ReactionNetworkProblem) end function AlgebraicAgents._projected_to(state::ReactionNetworkProblem) - state.t > state.tspan[2] ? true : state.t + return state.t > state.tspan[2] ? true : state.t end function fetch_params(acs::ReactionNetworkSchema) @@ -416,4 +423,4 @@ function fetch_params(acs::ReactionNetworkSchema) acs[i, :prmName] => acs[i, :prmVal] for i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P)) )) -end \ No newline at end of file +end diff --git a/src/state.jl b/src/state.jl index 0aba482..8d43eef 100644 --- a/src/state.jl +++ b/src/state.jl @@ -73,9 +73,7 @@ function init_u!(state::ReactionNetworkProblem) foreach(i -> u[i] = state[i, :specInitVal], 1:nparts(state, :S)); state.u = u) end -function save!(state::ReactionNetworkProblem) - return push!(state.sol, (state.t, state.u[:]...)) -end +save!(state::ReactionNetworkProblem) = push!(state.sol, (state.t, state.u[:]...)) function compile_observables(acs::ReactionNetworkSchema) observables = Dict{Symbol,Observable}() @@ -224,4 +222,4 @@ set_params(state::ReactionNetworkProblem, vals...) = function add_to_spawn!(state::ReactionNetworkProblem, hash, n) ix = findfirst(ix -> state.transition_recipes[:transHash][ix] == hash) return !isnothing(ix) && (state.transition_recipes[:transHash][ix] += n) -end \ No newline at end of file +end diff --git a/tutorial/basics.jl b/tutorial/basics.jl index de13b55..bc1ce13 100644 --- a/tutorial/basics.jl +++ b/tutorial/basics.jl @@ -18,4 +18,5 @@ simulate(prob) # access solution prob.sol +# plot solution draw(prob) From 033f2020507ed2d40363d9dfeea15b53db7153e9 Mon Sep 17 00:00:00 2001 From: Sean <10673535+slwu89@users.noreply.github.com> Date: Tue, 10 Oct 2023 08:02:45 -0700 Subject: [PATCH 07/11] Fixes for compatibility with latest Catlab/ACSets (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixes for new versions of Catlab/ACSets * drop Catlab dep, add ACSets; use parts rather than 1:nparts * fix tutorials and tests --------- Co-authored-by: Bíma, Jan --- Project.toml | 4 +- docs/build/index.html | 2 +- docs/build/search_index.js | 2 +- readme.md | 6 +- src/ReactiveDynamics.jl | 106 +++++++++++++-------------- src/compilers.jl | 2 +- src/interface/update.jl | 2 +- src/loadsave.jl | 11 ++- src/operators/equalize.jl | 2 +- src/operators/joins.jl | 8 +- src/solvers.jl | 33 +++++---- src/state.jl | 23 ++++-- test/tutorial_tests.jl | 2 + tutorial/example.jl | 30 +++----- tutorial/joins/joins.jl | 10 +-- tutorial/joins/submodel.jl | 2 +- tutorial/loadsave/loadsave.jl | 8 +- tutorial/optimize/optimize.jl | 2 +- tutorial/optimize/optimize_custom.jl | 4 +- tutorial/rd_example.jl | 4 +- tutorial/toy_pharma_model.jl | 24 ++---- 21 files changed, 143 insertions(+), 144 deletions(-) diff --git a/Project.toml b/Project.toml index 5385d89..87db175 100644 --- a/Project.toml +++ b/Project.toml @@ -3,9 +3,9 @@ uuid = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4" version = "0.2.7" [deps] +ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8" AlgebraicAgents = "f6eb0ae3-10fa-40e6-88dd-9006ba45093a" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" @@ -28,8 +28,8 @@ TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] +ACSets = "0.2.6" CSV = "0.10" -Catlab = "0.14" ComponentArrays = "0.14" Crayons = "4.1" DataFrames = "1.6" diff --git a/docs/build/index.html b/docs/build/index.html index 93c5e9e..991dc9f 100644 --- a/docs/build/index.html +++ b/docs/build/index.html @@ -1,5 +1,5 @@ -API Documentation · ReactiveDynamics.jl API Documentation

API Documentation

Create a model

ReactiveDynamics.@ReactionNetworkMacro

Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.

Most arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.

Custom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.

Examples

acs = @ReactionNetwork begin
+API Documentation · ReactiveDynamics.jl API Documentation

API Documentation

Create a model

ReactiveDynamics.@ReactionNetworkMacro

Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.

Most arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.

Custom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.

Examples

acs = @ReactionNetworkSchema begin
     1.0, X ⟶ Y
     1.0, X ⟶ Y, priority=>6., prob=>.7, capacity=>3.
     1.0, ∅ --> (Poisson(.3γ)X, Poisson(.5)Y)
diff --git a/docs/build/search_index.js b/docs/build/search_index.js
index addd92b..6b6953e 100644
--- a/docs/build/search_index.js
+++ b/docs/build/search_index.js
@@ -1,3 +1,3 @@
 var documenterSearchIndex = {"docs":
-[{"location":"index.html#API-Documentation","page":"API Documentation","title":"API Documentation","text":"","category":"section"},{"location":"index.html#Create-a-model","page":"API Documentation","title":"Create a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@ReactionNetwork","category":"page"},{"location":"index.html#ReactiveDynamics.@ReactionNetwork","page":"API Documentation","title":"ReactiveDynamics.@ReactionNetwork","text":"Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.\n\nMost arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.\n\nCustom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.\n\nExamples\n\nacs = @ReactionNetwork begin\n    1.0, X ⟶ Y\n    1.0, X ⟶ Y, priority=>6., prob=>.7, capacity=>3.\n    1.0, ∅ --> (Poisson(.3γ)X, Poisson(.5)Y)\n    (XY > 100) && (XY -= 1)\nend\n@push acs 1.0 X ⟶ Y \n@prob_init acs X=1 Y=2 XY=α\n@prob_params acs γ=1 α=4\n@solve_and_plot acs\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Modify-a-model","page":"API Documentation","title":"Modify a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common transition attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\ntransPriority priority of a transition (influences resource allocation)\ntransProbOfSuccess probability that a transition terminates successfully\ntransCapacity maximum number of concurrent instances of the transition\ntransCycleTime duration of a transition's instance (adjusted by resource allocation)\ntransMaxLifeTime maximal duration of a transition's instance\ntransPostAction action to be executed once a transition's instance terminates\ntransName name of a transition","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common species attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\nspecInitUncertainty uncertainty about variable's initial state (modelled as Gaussian standard deviation)\nspecInitVal initial value of a variable","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"Moreover, it is possible to specify the semantics of the \"rate\" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"For deterministic \"rates\", use @per_step(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@add_species\n@aka\n@mode\n@name_transition","category":"page"},{"location":"index.html#ReactiveDynamics.@add_species","page":"API Documentation","title":"ReactiveDynamics.@add_species","text":"Add new species to a model.\n\nExamples\n\n@add_species acs S I R\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@aka","page":"API Documentation","title":"ReactiveDynamics.@aka","text":"Alias object name in an acs.\n\nDefault names\n\nname short name\nspecies S\ntransition T\naction A\nevent E\nparam P\nmeta M\n\nExamples\n\n@aka acs species=resource transition=reaction\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@mode","page":"API Documentation","title":"ReactiveDynamics.@mode","text":"Set species modality.\n\nSupported modalities\n\nnonblock\nconserved\nrate\n\nExamples\n\n@mode acs (r\"proj\\w+\", r\"experimental\\w+\") conserved\n@mode acs (S, I) conserved\n@mode acs S conserved\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@name_transition","page":"API Documentation","title":"ReactiveDynamics.@name_transition","text":"Set name of a transition in the model.\n\nExamples\n\n@name_transition acs 1=\"name\"\n@name_transition acs name=\"transition_name\"\n@name_transition acs \"name\"=\"transition_name\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Resource-costs","page":"API Documentation","title":"Resource costs","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@cost\n@valuation\n@reward","category":"page"},{"location":"index.html#ReactiveDynamics.@cost","page":"API Documentation","title":"ReactiveDynamics.@cost","text":"Set cost.\n\nExamples\n\n@cost model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@valuation","page":"API Documentation","title":"ReactiveDynamics.@valuation","text":"Set valuation.\n\nExamples\n\n@valuation model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@reward","page":"API Documentation","title":"ReactiveDynamics.@reward","text":"Set reward.\n\nExamples\n\n@reward model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Add-reactions","page":"API Documentation","title":"Add reactions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@push\n@jump\n@periodic","category":"page"},{"location":"index.html#ReactiveDynamics.@push","page":"API Documentation","title":"ReactiveDynamics.@push","text":"Add reactions to an acset.\n\nExamples\n\n@push sir_acs β*S*I*tdecay(@time()) S+I --> 2I name=>SI2I\n@push sir_acs begin \n    ν*I, I --> R, name=>I2R\n    γ, R --> S, name=>R2S\nend\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@jump","page":"API Documentation","title":"ReactiveDynamics.@jump","text":"Add a jump process (with specified Poisson intensity per unit time step) to a model.\n\nExamples\n\n@jump acs λ Z += rand(Poisson(1.))\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@periodic","page":"API Documentation","title":"ReactiveDynamics.@periodic","text":"Add a periodic callback to a model.\n\nExamples\n\n@periodic acs 1. X += 1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Set-initial-values,-uncertainty,-and-solver-arguments","page":"API Documentation","title":"Set initial values, uncertainty, and solver arguments","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@prob_init\n@prob_uncertainty\n@prob_params\n@prob_meta","category":"page"},{"location":"index.html#ReactiveDynamics.@prob_init","page":"API Documentation","title":"ReactiveDynamics.@prob_init","text":"Set initial values of species in an acset.\n\nExamples\n\n@prob_init acs X=1 Y=2 Z=h(α)\n@prob_init acs [1., 2., 3.]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_uncertainty","page":"API Documentation","title":"ReactiveDynamics.@prob_uncertainty","text":"Set uncertainty in initial values of species in an acset (stderr).\n\nExamples\n\n@prob_uncertainty acs X=.1 Y=.2\n@prob_uncertainty acs [.1, .2,]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_params","page":"API Documentation","title":"ReactiveDynamics.@prob_params","text":"Set parameter values in an acset.\n\nExamples\n\n@prob_params acs α=1. β=2.\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_meta","page":"API Documentation","title":"ReactiveDynamics.@prob_meta","text":"Set model metadata (e.g. solver arguments)\n\nExamples\n\n@prob_meta acs tspan=(0, 100.) schedule=schedule_weighted!\n@prob_meta sir_acs tspan=250 tstep=1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-unions","page":"API Documentation","title":"Model unions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@join\n@equalize","category":"page"},{"location":"index.html#ReactiveDynamics.@join","page":"API Documentation","title":"ReactiveDynamics.@join","text":"@join models... [equalize...]\n\nPerforms join of models and identifies model variables, as specified.\n\nModel variables / parameter values and metadata are propagated; the last model takes precedence.\n\nExamples\n\n@join acs1 acs2 @catchall(A)=acs2.Z @catchall(XY) @catchall(B)\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@equalize","page":"API Documentation","title":"ReactiveDynamics.@equalize","text":"Identify (collapse) a set of species in a model.\n\nExamples\n\n@join acs acs1.A=acs2.A B=C\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-import-and-export","page":"API Documentation","title":"Model import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_network\n@export_network","category":"page"},{"location":"index.html#ReactiveDynamics.@import_network","page":"API Documentation","title":"ReactiveDynamics.@import_network","text":"Import a model from a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@import_network \"model.toml\"\n@import_network \"csv/model.toml\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_network","page":"API Documentation","title":"ReactiveDynamics.@export_network","text":"Export model to a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@export_network acs \"acs_data.toml\" # as a TOML\n@export_network acs \"csv/model.csv\" # as a CSV\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Solution-import-and-export","page":"API Documentation","title":"Solution import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_solution\n@export_solution_as_table\n@export_solution_as_csv\n@export_solution","category":"page"},{"location":"index.html#ReactiveDynamics.@import_solution","page":"API Documentation","title":"ReactiveDynamics.@import_solution","text":"@import_solution \"sol.jld2\"\n@import_solution \"sol.jld2\" sol\n\nImport a solution from a file.\n\nExamples\n\n@import_solution \"sir_acs_sol/serialized/sol.jld2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_table","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_table","text":"@export_solution_as_table sol\n\nExport a solution as a DataFrame.\n\nExamples\n\n@export_solution_as_table sol\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_csv","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_csv","text":"@export_solution_as_csv sol\n@export_solution_as_csv sol \"sol.csv\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution_as_csv sol \"sol.csv\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution","page":"API Documentation","title":"ReactiveDynamics.@export_solution","text":"@export_solution sol\n@export_solution sol \"sol.jld2\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution sol \"sol.jdl2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Problematize,sSolve,-and-plot","page":"API Documentation","title":"Problematize,sSolve, and plot","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@problematize\n@solve\n@plot","category":"page"},{"location":"index.html#ReactiveDynamics.@problematize","page":"API Documentation","title":"ReactiveDynamics.@problematize","text":"Convert a model to a DiscreteProblem. If passed a problem instance, return the instance.\n\nExamples\n\n@problematize acs tspan=1:100\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@solve","page":"API Documentation","title":"ReactiveDynamics.@solve","text":"Solve the problem. Solverargs passed at the calltime take precedence.\n\nExamples\n\n@solve prob\n@solve prob tspan=1:100\n@solve prob tspan=100 trajectories=20\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@plot","page":"API Documentation","title":"ReactiveDynamics.@plot","text":"Plot the solution (summary).\n\nExamples\n\n@plot sol plot_type=summary\n@plot sol plot_type=allocation # not supported for ensemble solutions!\n@plot sol plot_type=valuations # not supported for ensemble solutions!\n@plot sol plot_type=new_transitions # not supported for ensemble solutions!\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Optimization-and-fitting","page":"API Documentation","title":"Optimization and fitting","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@optimize\n@fit\n@fit_and_plot\n@build_solver","category":"page"},{"location":"index.html#ReactiveDynamics.@optimize","page":"API Documentation","title":"ReactiveDynamics.@optimize","text":"@optimize acset objective ... ... opts...\n\nTake an acset and optimize given functional.\n\nObjective is an expression which may reference the model's variables and parameters, i.e., A+β. The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nBy default, the functional is minimized. Specify objective=max to perform maximization. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\n@optimize acs abs(A-B) A B=20. α=2. lower_bounds=0 upper_bounds=100\n@optimize acss abs(A-B) A B=20. α=2. upper_bounds=[200,300,400] maxeval=200 objective=min\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit","page":"API Documentation","title":"ReactiveDynamics.@fit","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset and fit initial values and parameters to empirical data.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit_and_plot","page":"API Documentation","title":"ReactiveDynamics.@fit_and_plot","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset, fit initial values and parameters to empirical data, and plot the result.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@build_solver","page":"API Documentation","title":"ReactiveDynamics.@build_solver","text":"@build_solver acset ... ... opts...\n\nTake an acset and export a solution as a function of free vars and free parameters.\n\nExamples\n\nsolver = @build_solver acs S α β # function of variable S and parameters α, β\nsolver([S, α, β])\n\n\n\n\n\n","category":"macro"}]
+[{"location":"index.html#API-Documentation","page":"API Documentation","title":"API Documentation","text":"","category":"section"},{"location":"index.html#Create-a-model","page":"API Documentation","title":"Create a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@ReactionNetwork","category":"page"},{"location":"index.html#ReactiveDynamics.@ReactionNetwork","page":"API Documentation","title":"ReactiveDynamics.@ReactionNetwork","text":"Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.\n\nMost arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.\n\nCustom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.\n\nExamples\n\nacs = @ReactionNetworkSchema begin\n    1.0, X ⟶ Y\n    1.0, X ⟶ Y, priority=>6., prob=>.7, capacity=>3.\n    1.0, ∅ --> (Poisson(.3γ)X, Poisson(.5)Y)\n    (XY > 100) && (XY -= 1)\nend\n@push acs 1.0 X ⟶ Y \n@prob_init acs X=1 Y=2 XY=α\n@prob_params acs γ=1 α=4\n@solve_and_plot acs\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Modify-a-model","page":"API Documentation","title":"Modify a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common transition attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\ntransPriority priority of a transition (influences resource allocation)\ntransProbOfSuccess probability that a transition terminates successfully\ntransCapacity maximum number of concurrent instances of the transition\ntransCycleTime duration of a transition's instance (adjusted by resource allocation)\ntransMaxLifeTime maximal duration of a transition's instance\ntransPostAction action to be executed once a transition's instance terminates\ntransName name of a transition","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common species attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\nspecInitUncertainty uncertainty about variable's initial state (modelled as Gaussian standard deviation)\nspecInitVal initial value of a variable","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"Moreover, it is possible to specify the semantics of the \"rate\" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"For deterministic \"rates\", use @per_step(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@add_species\n@aka\n@mode\n@name_transition","category":"page"},{"location":"index.html#ReactiveDynamics.@add_species","page":"API Documentation","title":"ReactiveDynamics.@add_species","text":"Add new species to a model.\n\nExamples\n\n@add_species acs S I R\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@aka","page":"API Documentation","title":"ReactiveDynamics.@aka","text":"Alias object name in an acs.\n\nDefault names\n\nname short name\nspecies S\ntransition T\naction A\nevent E\nparam P\nmeta M\n\nExamples\n\n@aka acs species=resource transition=reaction\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@mode","page":"API Documentation","title":"ReactiveDynamics.@mode","text":"Set species modality.\n\nSupported modalities\n\nnonblock\nconserved\nrate\n\nExamples\n\n@mode acs (r\"proj\\w+\", r\"experimental\\w+\") conserved\n@mode acs (S, I) conserved\n@mode acs S conserved\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@name_transition","page":"API Documentation","title":"ReactiveDynamics.@name_transition","text":"Set name of a transition in the model.\n\nExamples\n\n@name_transition acs 1=\"name\"\n@name_transition acs name=\"transition_name\"\n@name_transition acs \"name\"=\"transition_name\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Resource-costs","page":"API Documentation","title":"Resource costs","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@cost\n@valuation\n@reward","category":"page"},{"location":"index.html#ReactiveDynamics.@cost","page":"API Documentation","title":"ReactiveDynamics.@cost","text":"Set cost.\n\nExamples\n\n@cost model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@valuation","page":"API Documentation","title":"ReactiveDynamics.@valuation","text":"Set valuation.\n\nExamples\n\n@valuation model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@reward","page":"API Documentation","title":"ReactiveDynamics.@reward","text":"Set reward.\n\nExamples\n\n@reward model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Add-reactions","page":"API Documentation","title":"Add reactions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@push\n@jump\n@periodic","category":"page"},{"location":"index.html#ReactiveDynamics.@push","page":"API Documentation","title":"ReactiveDynamics.@push","text":"Add reactions to an acset.\n\nExamples\n\n@push sir_acs β*S*I*tdecay(@time()) S+I --> 2I name=>SI2I\n@push sir_acs begin \n    ν*I, I --> R, name=>I2R\n    γ, R --> S, name=>R2S\nend\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@jump","page":"API Documentation","title":"ReactiveDynamics.@jump","text":"Add a jump process (with specified Poisson intensity per unit time step) to a model.\n\nExamples\n\n@jump acs λ Z += rand(Poisson(1.))\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@periodic","page":"API Documentation","title":"ReactiveDynamics.@periodic","text":"Add a periodic callback to a model.\n\nExamples\n\n@periodic acs 1. X += 1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Set-initial-values,-uncertainty,-and-solver-arguments","page":"API Documentation","title":"Set initial values, uncertainty, and solver arguments","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@prob_init\n@prob_uncertainty\n@prob_params\n@prob_meta","category":"page"},{"location":"index.html#ReactiveDynamics.@prob_init","page":"API Documentation","title":"ReactiveDynamics.@prob_init","text":"Set initial values of species in an acset.\n\nExamples\n\n@prob_init acs X=1 Y=2 Z=h(α)\n@prob_init acs [1., 2., 3.]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_uncertainty","page":"API Documentation","title":"ReactiveDynamics.@prob_uncertainty","text":"Set uncertainty in initial values of species in an acset (stderr).\n\nExamples\n\n@prob_uncertainty acs X=.1 Y=.2\n@prob_uncertainty acs [.1, .2,]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_params","page":"API Documentation","title":"ReactiveDynamics.@prob_params","text":"Set parameter values in an acset.\n\nExamples\n\n@prob_params acs α=1. β=2.\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_meta","page":"API Documentation","title":"ReactiveDynamics.@prob_meta","text":"Set model metadata (e.g. solver arguments)\n\nExamples\n\n@prob_meta acs tspan=(0, 100.) schedule=schedule_weighted!\n@prob_meta sir_acs tspan=250 tstep=1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-unions","page":"API Documentation","title":"Model unions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@join\n@equalize","category":"page"},{"location":"index.html#ReactiveDynamics.@join","page":"API Documentation","title":"ReactiveDynamics.@join","text":"@join models... [equalize...]\n\nPerforms join of models and identifies model variables, as specified.\n\nModel variables / parameter values and metadata are propagated; the last model takes precedence.\n\nExamples\n\n@join acs1 acs2 @catchall(A)=acs2.Z @catchall(XY) @catchall(B)\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@equalize","page":"API Documentation","title":"ReactiveDynamics.@equalize","text":"Identify (collapse) a set of species in a model.\n\nExamples\n\n@join acs acs1.A=acs2.A B=C\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-import-and-export","page":"API Documentation","title":"Model import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_network\n@export_network","category":"page"},{"location":"index.html#ReactiveDynamics.@import_network","page":"API Documentation","title":"ReactiveDynamics.@import_network","text":"Import a model from a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@import_network \"model.toml\"\n@import_network \"csv/model.toml\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_network","page":"API Documentation","title":"ReactiveDynamics.@export_network","text":"Export model to a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@export_network acs \"acs_data.toml\" # as a TOML\n@export_network acs \"csv/model.csv\" # as a CSV\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Solution-import-and-export","page":"API Documentation","title":"Solution import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_solution\n@export_solution_as_table\n@export_solution_as_csv\n@export_solution","category":"page"},{"location":"index.html#ReactiveDynamics.@import_solution","page":"API Documentation","title":"ReactiveDynamics.@import_solution","text":"@import_solution \"sol.jld2\"\n@import_solution \"sol.jld2\" sol\n\nImport a solution from a file.\n\nExamples\n\n@import_solution \"sir_acs_sol/serialized/sol.jld2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_table","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_table","text":"@export_solution_as_table sol\n\nExport a solution as a DataFrame.\n\nExamples\n\n@export_solution_as_table sol\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_csv","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_csv","text":"@export_solution_as_csv sol\n@export_solution_as_csv sol \"sol.csv\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution_as_csv sol \"sol.csv\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution","page":"API Documentation","title":"ReactiveDynamics.@export_solution","text":"@export_solution sol\n@export_solution sol \"sol.jld2\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution sol \"sol.jdl2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Problematize,sSolve,-and-plot","page":"API Documentation","title":"Problematize,sSolve, and plot","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@problematize\n@solve\n@plot","category":"page"},{"location":"index.html#ReactiveDynamics.@problematize","page":"API Documentation","title":"ReactiveDynamics.@problematize","text":"Convert a model to a DiscreteProblem. If passed a problem instance, return the instance.\n\nExamples\n\n@problematize acs tspan=1:100\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@solve","page":"API Documentation","title":"ReactiveDynamics.@solve","text":"Solve the problem. Solverargs passed at the calltime take precedence.\n\nExamples\n\n@solve prob\n@solve prob tspan=1:100\n@solve prob tspan=100 trajectories=20\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@plot","page":"API Documentation","title":"ReactiveDynamics.@plot","text":"Plot the solution (summary).\n\nExamples\n\n@plot sol plot_type=summary\n@plot sol plot_type=allocation # not supported for ensemble solutions!\n@plot sol plot_type=valuations # not supported for ensemble solutions!\n@plot sol plot_type=new_transitions # not supported for ensemble solutions!\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Optimization-and-fitting","page":"API Documentation","title":"Optimization and fitting","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@optimize\n@fit\n@fit_and_plot\n@build_solver","category":"page"},{"location":"index.html#ReactiveDynamics.@optimize","page":"API Documentation","title":"ReactiveDynamics.@optimize","text":"@optimize acset objective ... ... opts...\n\nTake an acset and optimize given functional.\n\nObjective is an expression which may reference the model's variables and parameters, i.e., A+β. The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nBy default, the functional is minimized. Specify objective=max to perform maximization. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\n@optimize acs abs(A-B) A B=20. α=2. lower_bounds=0 upper_bounds=100\n@optimize acss abs(A-B) A B=20. α=2. upper_bounds=[200,300,400] maxeval=200 objective=min\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit","page":"API Documentation","title":"ReactiveDynamics.@fit","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset and fit initial values and parameters to empirical data.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit_and_plot","page":"API Documentation","title":"ReactiveDynamics.@fit_and_plot","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset, fit initial values and parameters to empirical data, and plot the result.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@build_solver","page":"API Documentation","title":"ReactiveDynamics.@build_solver","text":"@build_solver acset ... ... opts...\n\nTake an acset and export a solution as a function of free vars and free parameters.\n\nExamples\n\nsolver = @build_solver acs S α β # function of variable S and parameters α, β\nsolver([S, α, β])\n\n\n\n\n\n","category":"macro"}]
 }
diff --git a/readme.md b/readme.md
index e09ef3e..ae2cf07 100644
--- a/readme.md
+++ b/readme.md
@@ -68,7 +68,7 @@ Follow the SIR model's reactions:
 using ReactiveDynamics
 
 # model dynamics
-sir_acs = @ReactionNetwork begin
+sir_acs = @ReactionNetworkSchema begin
         α*S*I, S+I --> 2I, name=>I2R
         β*I, I --> R, name=>R2S 
 end
@@ -196,7 +196,7 @@ To harness the capabilities of **GeneratedExpressions.jl**, let us first declare
 end
     
 # generate submodel dynamics
-push!(rd_models, @ReactionNetwork begin
+push!(rd_models, @ReactionNetworkSchema begin
                 M[$i][$m, $n], state[$m] + {demand[$i][$m, $n, $l]*resource[$l], l=1:$r, dlm=+} --> state[$n] + 
                         {production[$i][$m, $n, $l]*resource[$l], l=1:$r, dlm=+}, cycle_time=>cycle_times[$i][$m, $n], probability_of_success=>$m*$n/(n[$i])^2
         end m=1:ReactiveDynamics.ns[$i] n=1:ReactiveDynamics.ns[$i]
@@ -292,7 +292,7 @@ end
 Next we set up a simple dynamics and supply initial parameters.
 
 ```julia
-acs = @ReactionNetwork begin
+acs = @ReactionNetworkSchema begin
     function_to_learn(A, B, C, params), A --> B+C
     1., B --> C
     2., C --> B
diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl
index b0eef6f..8a52ed1 100644
--- a/src/ReactiveDynamics.jl
+++ b/src/ReactiveDynamics.jl
@@ -1,6 +1,6 @@
 module ReactiveDynamics
 
-using Catlab, Catlab.CategoricalAlgebra, Catlab.Present
+using ACSets
 using Reexport
 using MacroTools
 using ComponentArrays
@@ -27,50 +27,50 @@ Base.@kwdef mutable struct FoldedObservable
     on::Vector{SampleableValues} = SampleableValues[]
 end
 
-@present TheoryReactionNetwork(FreeSchema) begin
-    (S, T)::Ob # species, transitions
-
-    (
-        SymbolicAttributeT,
-        DescriptiveAttributeT,
-        SampleableAttributeT,
-        ModalityAttributeT,
-        PcsOptT,
-        PrmAttributeT,
-    )::AttrType
-
-    specName::Attr(S, SymbolicAttributeT)
-    specModality::Attr(S, ModalityAttributeT)
-    specInitVal::Attr(S, SampleableAttributeT)
-    specInitUncertainty::Attr(S, SampleableAttributeT)
-    (specCost, specReward, specValuation)::Attr(S, SampleableAttributeT)
-
-    trans::Attr(T, SampleableAttributeT)
-    transPriority::Attr(T, SampleableAttributeT)
-    transRate::Attr(T, SampleableAttributeT)
-    transCycleTime::Attr(T, SampleableAttributeT)
-    transProbOfSuccess::Attr(T, SampleableAttributeT)
-    transCapacity::Attr(T, SampleableAttributeT)
-    transMaxLifeTime::Attr(T, SampleableAttributeT)
-    transPostAction::Attr(T, SampleableAttributeT)
-    transMultiplier::Attr(T, SampleableAttributeT)
-    transName::Attr(T, DescriptiveAttributeT)
-
-    E::Ob # events
-    (eventTrigger, eventAction)::Attr(E, SampleableAttributeT)
-
-    obs::Ob # processes (observables)
-    obsName::Attr(obs, SymbolicAttributeT)
-    obsOpts::Attr(obs, PcsOptT)
-
-    (P, M)::Ob # model params, solver args
-
-    prmName::Attr(P, SymbolicAttributeT)
-    prmVal::Attr(P, PrmAttributeT)
-
-    metaKeyword::Attr(M, SymbolicAttributeT)
-    metaVal::Attr(M, SampleableAttributeT)
-end
+TheoryReactionNetwork = BasicSchema(
+    [:S, :T, :E, :obs, :P, :M], # species, transitions, events, processes (observables), model params, solver args
+    [], # no homs
+    [
+        :SymbolicAttributeT,
+        :DescriptiveAttributeT,
+        :SampleableAttributeT,
+        :ModalityAttributeT,
+        :PcsOptT,
+        :PrmAttributeT,
+    ], # AttrTypes
+    [
+        # species
+        (:specName, :S, :SymbolicAttributeT),
+        (:specModality, :S, :ModalityAttributeT),
+        (:specInitVal, :S, :SampleableAttributeT),
+        (:specInitUncertainty, :S, :SampleableAttributeT),
+        (:specCost, :S, :SampleableAttributeT),
+        (:specReward, :S, :SampleableAttributeT),
+        (:specValuation, :S, :SampleableAttributeT),
+        # transitions
+        (:trans, :T, :SampleableAttributeT),
+        (:transPriority, :T, :SampleableAttributeT),
+        (:transRate, :T, :SampleableAttributeT),
+        (:transCycleTime, :T, :SampleableAttributeT),
+        (:transProbOfSuccess, :T, :SampleableAttributeT),
+        (:transCapacity, :T, :SampleableAttributeT),
+        (:transMaxLifeTime, :T, :SampleableAttributeT),
+        (:transPostAction, :T, :SampleableAttributeT),
+        (:transMultiplier, :T, :SampleableAttributeT),
+        (:transName, :T, :DescriptiveAttributeT),
+        # events
+        (:eventTrigger, :E, :SampleableAttributeT),
+        (:eventAction, :E, :SampleableAttributeT),
+        # observables
+        (:obsName, :obs, :SymbolicAttributeT),
+        (:obsOpts, :obs, :PcsOptT),
+        # params, args
+        (:prmName, :P, :SymbolicAttributeT),
+        (:prmVal, :P, :PrmAttributeT),
+        (:metaKeyword, :M, :SymbolicAttributeT),
+        (:metaVal, :M, :SampleableAttributeT),
+    ],
+)
 
 @acset_type FoldedReactionNetworkType(TheoryReactionNetwork)
 
@@ -93,6 +93,7 @@ Base.convert(::Type{Union{String,Symbol,Missing}}, ex::String) =
     end
 
 Base.convert(::Type{SampleableValues}, ex::String) = MacroTools.striplines(Meta.parse(ex))
+
 Base.convert(::Type{Set{Symbol}}, ex::String) = eval(Meta.parse(ex))
 Base.convert(::Type{FoldedObservable}, ex::String) = eval(Meta.parse(ex))
 
@@ -137,23 +138,18 @@ species_modalities = [:nonblock, :conserved, :rate]
 
 function assign_defaults!(acs::ReactionNetworkSchema)
     for (_, v_) in defargs, (k, v) in v_
-        for i = 1:length(subpart(acs, k))
-            isnothing(acs[i, k]) && (subpart(acs, k)[i] = v)
+        for i in dom_parts(acs, k)
+            isnothing(acs[i, k]) && (acs[i, k] = v)
         end
     end
 
     foreach(
-        i ->
-            !isnothing(acs[i, :specModality]) ||
-                (subpart(acs, :specModality)[i] = Set{Symbol}()),
-        1:nparts(acs, :S),
+        i -> !isnothing(acs[i, :specModality]) || (acs[i, :specModality] = Set{Symbol}()),
+        parts(acs, :S),
     )
     k = [:specCost, :specReward, :specValuation]
     foreach(
-        k -> foreach(
-            i -> !isnothing(acs[i, k]) || (subpart(acs, k)[i] = 0.0),
-            1:nparts(acs, :S),
-        ),
+        k -> foreach(i -> !isnothing(acs[i, k]) || (acs[i, k] = 0.0), parts(acs, :S)),
         k,
     )
 
diff --git a/src/compilers.jl b/src/compilers.jl
index 9edccc6..46cb842 100644
--- a/src/compilers.jl
+++ b/src/compilers.jl
@@ -164,7 +164,7 @@ function compile_attrs(acs::ReactionNetworkSchema)
     transitions[:transActivated] = fill(true, nparts(acs, :T))
     transitions[:transToSpawn] = zeros(nparts(acs, :T))
     transitions[:transHash] =
-        [coalesce(acs[i, :transName], gensym()) for i = 1:nparts(acs, :T)]
+        [coalesce(acs[i, :transName], gensym()) for i in parts(acs, :T)]
 
     return attrs, transitions, wrap_fun
 end
diff --git a/src/interface/update.jl b/src/interface/update.jl
index ce4e811..cf3c354 100644
--- a/src/interface/update.jl
+++ b/src/interface/update.jl
@@ -70,7 +70,7 @@ macro name_transition(acsex, exs...)
                 acs = $(esc(acsex))
                 ixs = findall(
                     i -> string(acs[i, :transName]) == $(string(ex.args[1])),
-                    1:nparts(acs, :T),
+                    parts(acs, :T),
                 )
                 foreach(i -> acs[i, :transName] = $(string(ex.args[2])), ixs)
             end
diff --git a/src/loadsave.jl b/src/loadsave.jl
index 89571fb..4002a24 100644
--- a/src/loadsave.jl
+++ b/src/loadsave.jl
@@ -28,7 +28,7 @@ function export_network(acs::ReactionNetworkSchema)
     dict = Dict()
     for (key, val) in objects_aliases
         push!(dict, val => [])
-        for i = 1:nparts(acs, key)
+        for i in parts(acs, key)
             dict_ = Dict()
             for attr in get_attrs(val)
                 attr_val = acs[i, Symbol(attr)]
@@ -44,13 +44,16 @@ function export_network(acs::ReactionNetworkSchema)
 end
 
 function load_network(dict::Dict)
-    acs = ReactionNetwork()
+    acs = ReactionNetworkSchema()
     for (key, val) in objects_aliases
         val == "prm" && continue
         for row in get(dict, val, [])
             i = add_part!(acs, key)
             for (attr, attrval) in row
                 set_subpart!(acs, i, Symbol(attr), attrval)
+                if (acs[i, Symbol(attr)] isa String && !(contains(attr, "name")))
+                    acs[i, Symbol(attr)] = MacroTools.striplines(Meta.parse(attrval))
+                end
             end
         end
     end
@@ -230,10 +233,10 @@ Export a solution as a `DataFrame`.
 ```
 """
 macro export_solution_as_table(solex, pathex = "sol.jld2")
-    return :(DataFrame($(esc(solex))))
+    return :(DataFrame($(esc(solex)).sol))
 end
 
-get_DataFrame(sol) = sol isa EnsembleSolution ? DataFrame(sol)[!, [:u, :t]] : DataFrame(sol)
+get_DataFrame(sol) = sol.sol
 
 """
     @export_solution_as_csv sol
diff --git a/src/operators/equalize.jl b/src/operators/equalize.jl
index e679ec9..c8e5724 100644
--- a/src/operators/equalize.jl
+++ b/src/operators/equalize.jl
@@ -27,7 +27,7 @@ function equalize!(acs::ReactionNetworkSchema, eqs = [])
         block_alias = findfirst(e -> e[1] == :alias, block)
         block_alias = !isnothing(block_alias) ? block[block_alias][2] : first(block)[2]
         species_ixs = Int64[]
-        for e in block, i = 1:nparts(acs, :S)
+        for e in block, i in parts(acs, :S)
             (
                 (i == e[2]) ||
                 (
diff --git a/src/operators/joins.jl b/src/operators/joins.jl
index f5c3276..5ac9808 100644
--- a/src/operators/joins.jl
+++ b/src/operators/joins.jl
@@ -10,7 +10,7 @@ Merge `acs2` onto `acs1`, the attributes in `acs2` taking precedence. Identify r
 function union_acs!(acs1, acs2, name = gensym("acs_"), eqs = [])
     acs2 = deepcopy(acs2)
     prepend!(acs2, name, eqs)
-    for i = 1:nparts(acs2, :S)
+    for i in parts(acs2, :S)
         inc = incident(acs1, acs2[i, :specName], :specName)
         isempty(inc) && (inc = add_part!(acs1, :S; specName = acs2[i, :specName]);
         assign_defaults!(acs1))
@@ -41,13 +41,13 @@ function union_acs!(acs1, acs2, name = gensym("acs_"), eqs = [])
         new_trans_ix,
     )
 
-    for i = 1:nparts(acs2, :P)
+    for i in parts(acs2, :P)
         inc = incident(acs1, acs2[i, :prmName], :prmName)
         isempty(inc) && (inc = add_part!(acs1, :P; prmName = acs2[i, :prmName]))
         !ismissing(acs2[i, :prmVal]) && (acs1[first(inc), :prmVal] = acs2[i, :prmVal])
     end
 
-    for i = 1:nparts(acs2, :M)
+    for i in parts(acs2, :M)
         inc = incident(acs1, acs2[i, :metaKeyword], :metaKeyword)
         isempty(inc) && (inc = add_part!(acs1, :M; metaKeyword = acs2[i, :metaKeyword]))
         !ismissing(acs2[i, :metaVal]) && (acs1[first(inc), :metaVal] = acs2[i, :metaVal])
@@ -61,7 +61,7 @@ Prepend species names with a model identifier (unless a global species name).
 """
 function prepend!(acs::ReactionNetworkSchema, name = gensym("acs"), eqs = [])
     specmap = Dict()
-    for i = 1:nparts(acs, :S)
+    for i in parts(acs, :S)
         new_name = normalize_name(name, i, acs[i, :specName], eqs)
         push!(specmap, acs[i, :specName] => (acs[i, :specName] = new_name))
     end
diff --git a/src/solvers.jl b/src/solvers.jl
index f53b001..270e8c7 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -132,11 +132,11 @@ function evolve!(state)
 
     foreach(
         i -> qs[i] = state[i, :transRate] * state[i, :transMultiplier],
-        1:nparts(state, :T),
+        parts(state, :T),
     )
     qs .= ceil.(Ref(Int), qs)
 
-    for i = 1:nparts(state, :T)
+    for i in parts(state, :T)
         new_instances = qs[i] + state[i, :transToSpawn]
         capacity =
             state[i, :transCapacity] -
@@ -164,7 +164,7 @@ function evolve!(state)
     actual_allocs .+= sum(allocs; dims = 2)
 
     # add spawned transitions to the heap
-    for i = 1:nparts(state, :T)
+    for i in parts(state, :T)
         qs[i] != 0 && push!(
             state.ongoing_transitions,
             Transition(
@@ -215,14 +215,14 @@ function evolve!(state)
         (
             :valuation_cost,
             state.t,
-            actual_allocs' * [state[i, :specCost] for i = 1:nparts(state, :S)],
+            actual_allocs' * [state[i, :specCost] for i in parts(state, :S)],
         ),
     )
 end
 
 # execute callbacks
 function event_action!(state)
-    for i = 1:nparts(state, :E)
+    for i in parts(state, :E)
         !isnothing(state[i, :eventTrigger]) && !isnothing(state[i, :eventAction]) ||
             continue
         v = state[i, :eventTrigger]
@@ -280,10 +280,10 @@ function finish!(state)
         )
 
         context_eval(state, trans_.trans[:transPostAction])
-        terminated_all[trans_[:transHash]] =
-            get(terminated_all, trans_[:transHash], 0) + trans_.q
-        terminated_success[trans_[:transHash]] =
-            get(terminated_success, trans_[:transHash], 0) + q
+        terminated_all[Symbol(trans_[:transHash])] =
+            get(terminated_all, Symbol(trans_[:transHash]), 0) + trans_.q
+        terminated_success[Symbol(trans_[:transHash])] =
+            get(terminated_success, Symbol(trans_[:transHash]), 0) + q
 
         ix += 1
     end
@@ -324,7 +324,7 @@ function ReactionNetworkProblem(
 )
     assign_defaults!(acs)
     keywords = Dict{Symbol,Any}([
-        acs[i, :metaKeyword] => acs[i, :metaVal] for i = 1:nparts(acs, :M) if
+        acs[i, :metaKeyword] => acs[i, :metaVal] for i in parts(acs, :M) if
         !isnothing(acs[i, :metaKeyword]) && !isnothing(acs[i, :metaVal])
     ])
     merge!(keywords, Dict(collect(kwargs)))
@@ -337,7 +337,7 @@ function ReactionNetworkProblem(
     transition_recipes = transitions
     u0_init = zeros(nparts(acs, :S))
 
-    for i = 1:nparts(acs, :S)
+    for i in parts(acs, :S)
         if !isnothing(acs[i, :specName]) && haskey(u0, acs[i, :specName])
             u0_init[i] = u0[acs[i, :specName]]
         else
@@ -345,6 +345,13 @@ function ReactionNetworkProblem(
         end
     end
 
+    prms = Dict{Symbol,Any}((
+        acs[i, :prmName] => acs[i, :prmVal] for
+        i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P))
+    ))
+
+    merge!(p, prms)
+
     ongoing_transitions = Transition[]
     log = NamedTuple[]
     observables = compile_observables(acs)
@@ -406,7 +413,7 @@ function AlgebraicAgents._step!(state::ReactionNetworkProblem)
         (
             :valuation,
             state.t,
-            state.u' * [state[i, :specValuation] for i = 1:nparts(state, :S)],
+            state.u' * [state[i, :specValuation] for i in parts(state, :S)],
         ),
     )
 
@@ -421,6 +428,6 @@ end
 function fetch_params(acs::ReactionNetworkSchema)
     return Dict{Symbol,Any}((
         acs[i, :prmName] => acs[i, :prmVal] for
-        i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P))
+        i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), parts(acs, :P))
     ))
 end
diff --git a/src/state.jl b/src/state.jl
index 8d43eef..97e85db 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -70,7 +70,7 @@ end
 
 function init_u!(state::ReactionNetworkProblem)
     return (u = fill(0.0, nparts(state, :S));
-    foreach(i -> u[i] = state[i, :specInitVal], 1:nparts(state, :S));
+    foreach(i -> u[i] = state[i, :specInitVal], parts(state, :S));
     state.u = u)
 end
 save!(state::ReactionNetworkProblem) = push!(state.sol, (state.t, state.u[:]...))
@@ -91,7 +91,10 @@ function compile_observables(acs::ReactionNetworkSchema)
             opts.range,
         )
 
-        push!(observables, name => Observable(name, -Inf, range, opts.every, on, missing))
+        push!(
+            observables,
+            name => Observable(string(name), -Inf, range, opts.every, on, missing),
+        )
     end
 
     return observables
@@ -131,7 +134,7 @@ end
 
 function prune_r_line(r_line)
     return if r_line isa Expr && r_line.args[1] ∈ fwd_arrows
-        r_line.args[2:3]
+        r_line.args[[2, 3]]
     elseif r_line isa Expr && r_line.args[1] ∈ bwd_arrows
         r_line.args[[3, 2]]
     elseif isexpr(r_line, :macrocall) && (macroname(r_line) == :choose)
@@ -149,7 +152,7 @@ function prune_r_line(r_line)
 end
 
 function find_index(species::Symbol, state::ReactionNetworkProblem)
-    return findfirst(i -> state[i, :specName] == species, 1:nparts(state, :S))
+    return findfirst(i -> state[i, :specName] == species, parts(state, :S))
 end
 
 function sample_transitions!(state::ReactionNetworkProblem)
@@ -195,8 +198,12 @@ function as_state(u, t, state::ReactionNetworkProblem)
     return (state = deepcopy(state); state.u .= u; state.t = t; state)
 end
 
-function Catlab.CategoricalAlgebra.nparts(state::ReactionNetworkProblem, obj::Symbol)
-    return obj == :T ? length(state.transitions[:transLHS]) : nparts(state.acs, obj)
+function ACSets.ACSetInterface.nparts(state::ReactionNetworkProblem, obj::Symbol)
+    return nparts(state.acs, obj)
+end
+
+function ACSets.ACSetInterface.parts(state::ReactionNetworkProblem, obj::Symbol)
+    return parts(state.acs, obj)
 end
 
 ## query the state
@@ -209,8 +216,8 @@ state(state::ReactionNetworkProblem) = state
 
 function periodic(state::ReactionNetworkProblem, period)
     return period == 0.0 || (
-        length(state.history_t) > 1 &&
-        (fld(state.t, period) - fld(state.history_t[end-1], period) > 0)
+        length(state.sol.t) > 1 &&
+        (fld(state.t, period) - fld(state.sol.t[end-1], period) > 0)
     )
 end
 
diff --git a/test/tutorial_tests.jl b/test/tutorial_tests.jl
index 03e4215..5f55109 100644
--- a/test/tutorial_tests.jl
+++ b/test/tutorial_tests.jl
@@ -1,5 +1,7 @@
 using ReactiveDynamics
 
+include("safeinclude.jl")
+
 @safeinclude "example" "../tutorial/example.jl"
 @safeinclude "joins" "../tutorial/joins/joins.jl"
 @safeinclude "loadsave" "../tutorial/loadsave/loadsave.jl"
diff --git a/tutorial/example.jl b/tutorial/example.jl
index b7fb3d1..c41c1cc 100644
--- a/tutorial/example.jl
+++ b/tutorial/example.jl
@@ -1,7 +1,7 @@
 using ReactiveDynamics
 
 # acs as a model : incomplete dynamics
-sir_acs = @ReactionNetwork
+sir_acs = @ReactionNetworkSchema
 
 # set up ontology: try ?@aka
 @aka sir_acs transition = reaction species = population_group
@@ -30,18 +30,14 @@ u0 = [999, 10, 0] # alternative specification
 @prob_params sir_acs β = 0.0001 ν = 0.01 γ = 5
 @prob_meta sir_acs tspan = 100
 
-#prob = @problematize sir_acs
-prob = @problematize sir_acs tspan = 200
+prob = ReactionNetworkProblem(sir_acs; tspan = 200)
 
-sol = @solve prob trajectories = 20
-@plot sol plot_type = summary
-sol = @solve prob
-@plot sol plot_type = allocation
-@plot sol plot_type = valuations
-@plot sol plot_type = new_transitions
+sol = simulate(prob)
+
+draw(prob)
 
 # acs as a model : incomplete dynamics
-sir_acs = @ReactionNetwork
+sir_acs = @ReactionNetworkSchema
 
 # set up ontology: try ?@aka
 @aka sir_acs transition = reaction species = population_group
@@ -65,7 +61,7 @@ u0 = [999, 10, 0] # alternative specification
 @prob_params sir_acs β = 0.0001 ν = 0.01 γ = 5
 
 # model dynamics
-sir_acs = @ReactionNetwork begin
+sir_acs = @ReactionNetworkSchema begin
     α * S * I, S + I --> 2I, cycle_time => 0, name => I2R
     β * I, I --> R, cycle_time => 0, name => R2S
 end
@@ -77,16 +73,14 @@ end
 @prob_meta sir_acs tspan = 250 dt = 0.1
 
 # batch simulation
-prob = @problematize sir_acs
-sol = @solve prob trajectories = 20
-@plot sol plot_type = summary
-@plot sol plot_type = summary show = :S
-@plot sol plot_type = summary c = :green xlimits = (0.0, 100.0)
+prob = ReactionNetworkProblem(sir_acs)
+sol = simulate(prob)
+draw(prob)
 
-acs_1 = @ReactionNetwork begin
+acs_1 = @ReactionNetworkSchema begin
     1.0, A --> B + C
 end
-acs_2 = @ReactionNetwork begin
+acs_2 = @ReactionNetworkSchema begin
     1.0, A --> B + C
 end
 
diff --git a/tutorial/joins/joins.jl b/tutorial/joins/joins.jl
index ee87d7a..e7789a9 100644
--- a/tutorial/joins/joins.jl
+++ b/tutorial/joins/joins.jl
@@ -2,7 +2,7 @@ using ReactiveDynamics
 ## setup the environment
 n_models = 5;
 r = 2; # number of submodels, resources
-rd_models = ReactiveDynamics.ReactionNetwork[] # submodels
+rd_models = ReactiveDynamics.ReactionNetworkSchema[] # submodels
 
 @register begin
     ns = Int[] # size of submodels
@@ -51,8 +51,6 @@ u0 = rand(1:1000, nparts(rd_model, :S))
 
 @prob_meta rd_model tspan = 10
 
-prob = @problematize rd_model
-sol = @solve prob trajectories = 2
-
-# plot "state" species only
-@plot sol plot_type = summary show = r"state"
+prob = ReactionNetworkProblem(sir_acs)
+sol = simulate(prob)
+draw(sol)
diff --git a/tutorial/joins/submodel.jl b/tutorial/joins/submodel.jl
index 1e8fc43..faf370a 100644
--- a/tutorial/joins/submodel.jl
+++ b/tutorial/joins/submodel.jl
@@ -13,7 +13,7 @@ end
 # generate submodel dynamics
 push!(
     rd_models,
-    @ReactionNetwork begin
+    @ReactionNetworkSchema begin
         M[$i][$m, $n],
         state[$m] + {demand[$i][$m, $n, $l] * resource[$l], l = 1:($r), dlm = +} -->
         state[$n] + {production[$i][$m, $n, $l] * resource[$l], l = 1:($r), dlm = +},
diff --git a/tutorial/loadsave/loadsave.jl b/tutorial/loadsave/loadsave.jl
index 23d2be7..d6c8ee6 100644
--- a/tutorial/loadsave/loadsave.jl
+++ b/tutorial/loadsave/loadsave.jl
@@ -5,15 +5,15 @@ using ReactiveDynamics
 @assert @isdefined sir_acs
 @assert isdefined(ReactiveDynamics, :tdecay)
 
-prob = @problematize sir_acs
-sol = @solve prob trajectories = 20
+prob = ReactionNetworkProblem(sir_acs)
+sol = simulate(prob)
 
 @import_network "csv/model.csv" sir_acs_
 @assert @isdefined sir_acs_
 @assert isdefined(ReactiveDynamics, :foo)
 
-prob_ = @problematize sir_acs_
-sol_ = @solve prob_ trajectories = 20
+prob_ = ReactionNetworkProblem(sir_acs_)
+sol_ = simulate(prob_)
 
 # export, import the solution
 @export_solution sol
diff --git a/tutorial/optimize/optimize.jl b/tutorial/optimize/optimize.jl
index 0766369..d8af528 100644
--- a/tutorial/optimize/optimize.jl
+++ b/tutorial/optimize/optimize.jl
@@ -1,7 +1,7 @@
 using ReactiveDynamics
 
 # solve for steady state
-acss = @ReactionNetwork begin
+acss = @ReactionNetworkSchema begin
     3.0, A --> A, priority => 0.6, name => aa
     1.0, B + 0.2 * A --> 2 * α * B, prob => 0.7, priority => 0.6, name => bb
     3.0, A + 2 * B --> 2 * C, prob => 0.7, priority => 0.7, name => cc
diff --git a/tutorial/optimize/optimize_custom.jl b/tutorial/optimize/optimize_custom.jl
index 47dc489..a9cb780 100644
--- a/tutorial/optimize/optimize_custom.jl
+++ b/tutorial/optimize/optimize_custom.jl
@@ -8,7 +8,7 @@ using ReactiveDynamics
     end
 end
 
-acs = @ReactionNetwork begin
+acs = @ReactionNetworkSchema begin
     function_to_learn(A, B, C, params), A --> B + C
     1.0, B --> C
     2.0, C --> B
@@ -35,7 +35,7 @@ data = [60 30 5]
         return [A, B, C]' * params + α # params: 3-element vector
     end
 end
-acs = @ReactionNetwork begin
+acs = @ReactionNetworkSchema begin
     learnt_function(A, B, C, params, α), A --> B + C, priority => 0.6
     1.0, B --> C
     2.0, C --> B
diff --git a/tutorial/rd_example.jl b/tutorial/rd_example.jl
index 64638b3..a9b1644 100644
--- a/tutorial/rd_example.jl
+++ b/tutorial/rd_example.jl
@@ -15,7 +15,7 @@ rd_model = @ReactionNetwork
     transitions .+= 0.1 * rand(n_phase, n_phase)
 end
 
-rd_model = @ReactionNetwork begin
+rd_model = @ReactionNetworkSchema begin
     $i, phase[$i] --> phase[$j], cycle_time => $i * $j
 end i = 1:3 j = 1:($i)
 
@@ -46,7 +46,7 @@ sol = @solve prob trajectories = 20
     resource = rand(1:10, k, k, r)
 end
 
-rd_model = @ReactionNetwork begin
+rd_model = @ReactionNetworkSchema begin
     M[$i, $j],
     mod[$i] +
     {resource[$i, $j, $k] * resource[$k], k = rand(1:(ReactiveDynamics.r)), dlm = +} -->
diff --git a/tutorial/toy_pharma_model.jl b/tutorial/toy_pharma_model.jl
index 4fc6c7e..e536d19 100644
--- a/tutorial/toy_pharma_model.jl
+++ b/tutorial/toy_pharma_model.jl
@@ -1,7 +1,7 @@
 using ReactiveDynamics
 
 # model dynamics
-toy_pharma_model = @ReactionNetwork begin
+toy_pharma_model = @ReactionNetworkSchema begin
     α(candidate_compound, marketed_drug, κ),
     3 * @conserved(scientist) + @rate(budget) --> candidate_compound,
     name => discovery,
@@ -35,20 +35,16 @@ end
 ## other arguments passed to the solver
 @prob_meta toy_pharma_model tspan = 250 dt = 0.1
 
-prob = @problematize toy_pharma_model
+prob = ReactionNetworkProblem(toy_pharma_model)
 
-sol = @solve prob trajectories = 20
+sol = simulate(prob)
 
-using Plots
-
-@plot sol plot_type = summary
-
-@plot sol plot_type = summary show = :marketed_drug
+draw(sol)
 
 ## for deterministic rates 
 
 # model dynamics
-toy_pharma_model = @ReactionNetwork begin
+toy_pharma_model = @ReactionNetworkSchema begin
     @per_step(α(candidate_compound, marketed_drug, κ)),
     3 * @conserved(scientist) + @rate(budget) --> candidate_compound,
     name => discovery,
@@ -82,12 +78,8 @@ end
 ## other arguments passed to the solver
 @prob_meta toy_pharma_model tspan = 250
 
-prob = @problematize toy_pharma_model
-
-sol = @solve prob trajectories = 20
-
-using Plots
+prob = ReactionNetworkProblem(toy_pharma_model)
 
-@plot sol plot_type = summary
+sol = simulate(prob)
 
-@plot sol plot_type = summary show = :marketed_drug
+draw(sol)

From 8652f3d7940a740b021e2a295da111f1c10da954 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= 
Date: Thu, 22 Feb 2024 19:48:46 +0100
Subject: [PATCH 08/11] Add structured agents and related examples

---
 Project.toml                                  |   2 +-
 src/ReactiveDynamics.jl                       |   7 +
 src/compilers.jl                              |  33 +-
 src/interface/agents.jl                       |  52 +++
 src/interface/create.jl                       |   2 +
 src/interface/reaction_parser.jl              |   4 +-
 src/operators/equalize.jl                     |   5 +-
 src/operators/joins.jl                        |  28 +-
 src/solvers.jl                                | 197 +++++++++---
 src/state.jl                                  |  39 ++-
 tutorial/agents-integration/Project.toml      |   9 +
 tutorial/agents-integration/agents.jl         | 101 ++++++
 .../agents-integration/agents_integration.jl  | 302 ++++++++++++++++++
 13 files changed, 697 insertions(+), 84 deletions(-)
 create mode 100644 src/interface/agents.jl
 create mode 100644 tutorial/agents-integration/Project.toml
 create mode 100644 tutorial/agents-integration/agents.jl
 create mode 100644 tutorial/agents-integration/agents_integration.jl

diff --git a/Project.toml b/Project.toml
index 87db175..0ad533d 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,6 +1,6 @@
 name = "ReactiveDynamics"
 uuid = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4"
-version = "0.2.7"
+version = "0.2.8"
 
 [deps]
 ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8"
diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl
index 8a52ed1..a7192bf 100644
--- a/src/ReactiveDynamics.jl
+++ b/src/ReactiveDynamics.jl
@@ -37,6 +37,7 @@ TheoryReactionNetwork = BasicSchema(
         :ModalityAttributeT,
         :PcsOptT,
         :PrmAttributeT,
+        :BoolAttributeT,
     ], # AttrTypes
     [
         # species
@@ -47,6 +48,7 @@ TheoryReactionNetwork = BasicSchema(
         (:specCost, :S, :SampleableAttributeT),
         (:specReward, :S, :SampleableAttributeT),
         (:specValuation, :S, :SampleableAttributeT),
+        (:specStructured, :S, :BoolAttributeT),
         # transitions
         (:trans, :T, :SampleableAttributeT),
         (:transPriority, :T, :SampleableAttributeT),
@@ -55,6 +57,7 @@ TheoryReactionNetwork = BasicSchema(
         (:transProbOfSuccess, :T, :SampleableAttributeT),
         (:transCapacity, :T, :SampleableAttributeT),
         (:transMaxLifeTime, :T, :SampleableAttributeT),
+        (:transPreAction, :T, :SampleableAttributeT),
         (:transPostAction, :T, :SampleableAttributeT),
         (:transMultiplier, :T, :SampleableAttributeT),
         (:transName, :T, :DescriptiveAttributeT),
@@ -81,6 +84,7 @@ const ReactionNetworkSchema = FoldedReactionNetworkType{
     Set{Symbol},
     FoldedObservable,
     Any,
+    Bool
 }
 
 Base.convert(::Type{Symbol}, ex::String) = Symbol(ex)
@@ -100,6 +104,7 @@ Base.convert(::Type{FoldedObservable}, ex::String) = eval(Meta.parse(ex))
 prettynames = Dict(
     :transRate => [:rate],
     :specInitUncertainty => [:uncertainty, :stoch, :stochasticity],
+    :transPreAction => [:preAction, :pre],
     :transPostAction => [:postAction, :post],
     :transName => [:name, :interpretation],
     :transPriority => [:priority],
@@ -117,6 +122,7 @@ defargs = Dict(
         :transCycleTime => 0.0,
         :transMaxLifeTime => Inf,
         :transMultiplier => 1,
+        :transPreAction => :(),
         :transPostAction => :(),
         :transName => missing,
     ),
@@ -126,6 +132,7 @@ defargs = Dict(
         :specCost => 0.0,
         :specReward => 0.0,
         :specValuation => 0.0,
+        :specStructured => false,
     ),
     :P => Dict{Symbol,Any}(:prmVal => missing),
     :M => Dict{Symbol,Any}(:metaVal => missing),
diff --git a/src/compilers.jl b/src/compilers.jl
index 46cb842..0c741d8 100644
--- a/src/compilers.jl
+++ b/src/compilers.jl
@@ -26,16 +26,17 @@ end
 Recursively substitute model variables. Subsitution pairs are specified in `varmap`.
 """
 function recursively_substitute_vars!(varmap, ex)
-    ex isa Symbol && return (haskey(varmap, ex) ? varmap[ex] : ex)
-    ex isa Expr && for i = 1:length(ex.args)
-        if ex.args[i] isa Expr
-            recursively_substitute_vars!(varmap, ex.args[i])
-        else
-            (
-                ex.args[i] isa Symbol &&
-                haskey(varmap, ex.args[i]) &&
-                (ex.args[i] = varmap[ex.args[i]])
-            )
+    if ex isa Symbol
+        return haskey(varmap, ex) ? varmap[ex] : ex
+    elseif ex isa Expr
+        for i = 1:length(ex.args)
+            if ex.args[i] isa Expr
+                ex.args[i] = recursively_substitute_vars!(varmap, ex.args[i])
+            else
+                if ex.args[i] isa Symbol && haskey(varmap, ex.args[i])
+                    ex.args[i] = varmap[ex.args[i]]
+                end
+            end
         end
     end
 
@@ -114,7 +115,12 @@ function wrap_expr(fex, species_names, prm_names, varmap)
 
     # the function shall be a function of the dynamic ReactionNetworkSchema structure: letex -> :(state -> $letex)
     # eval the expression to a Julia function, save that function into the "compiled" acset
-    return eval(:(state -> $letex))
+
+    return eval(quote
+        function (state, transition)
+            $letex
+        end
+    end)
 end
 
 function get_wrap_fun(acs::ReactionNetworkSchema)
@@ -133,8 +139,9 @@ function skip_compile(attr)
            (string(attr) == "trans")
 end
 
-function compile_attrs(acs::ReactionNetworkSchema)
-    species_names = collect(acs[:, :specName])
+function compile_attrs(acs::ReactionNetworkSchema, structured_species)
+    species_names = setdiff(collect(acs[:, :specName]), structured_species)
+
     prm_names = collect(acs[:, :prmName])
     varmap = Dict([name => :(state.u[$i]) for (i, name) in enumerate(species_names)])
     for name in prm_names
diff --git a/src/interface/agents.jl b/src/interface/agents.jl
new file mode 100644
index 0000000..6233f63
--- /dev/null
+++ b/src/interface/agents.jl
@@ -0,0 +1,52 @@
+export AbstractStructuredSpecies, BaseStructuredSpecies
+export @structured
+export add_structured_species!
+
+# Abstract supertype of all structured species.
+abstract type AbstractStructuredSpecies <: AbstractAlgebraicAgent end
+
+# It comes handy to keep track of the transition the entity is assigned to (if).
+# In general, we will probably assume that each "structured agent" type implements this field.
+# Otherwise, it would be possible to implement getter and setter interface and use it from within ReaDyn.
+@aagent FreeAgent struct BaseStructuredSpecies
+    bound_transition::Union{Nothing, ReactiveDynamics.Transition}
+end
+
+# We use this to let the network know that the type is structured.
+function register_structured_species!(reaction_network, type)
+    if !(type ∈ reaction_network[:, :specName])
+        add_part!(reaction_network, :S; specName = type)
+    end
+
+    i = first(incident(reaction_network, type, :specName))
+    reaction_network[i, :specStructured] = true
+
+    return nothing
+end
+
+# Convenience macro to define structured species.
+macro structured(network, type)
+    name = Docs.namify(type.args[2])
+    
+    quote       
+        $(AlgebraicAgents.aagent(BaseStructuredSpecies, AbstractStructuredSpecies, type, ReactiveDynamics))
+        register_structured_species!($(esc(network)), $(QuoteNode(name)))
+    end
+end
+
+# Add a structured agent instance to an instance of a reaction network.
+function add_structured_species!(problem::ReactionNetworkProblem, agent)
+    entangle!(getagent(problem, "structured/$(nameof(typeof(agent)))"), agent)
+end
+
+import AlgebraicAgents
+
+# By default, structured agents have no evolutionary rule.
+AlgebraicAgents._projected_to(::AbstractStructuredSpecies) = nothing
+AlgebraicAgents._step!(::AbstractStructuredSpecies) = nothing
+
+# Tell if an agent is assigned to a transition, as a resource.
+isblocked(a) = !isnothing(a.bound_transition)
+
+# Priority with which an unbound agent will be assigned to a transition.
+priority(a, transition) = 0.0
\ No newline at end of file
diff --git a/src/interface/create.jl b/src/interface/create.jl
index ec49f85..099ca1e 100644
--- a/src/interface/create.jl
+++ b/src/interface/create.jl
@@ -295,6 +295,8 @@ function recursively_find_reactants!(reactants, pcs, ex::SampleableValues)
         end
     elseif isexpr(ex, :macrocall)
         recursively_find_reactants!(reactants, pcs, ex.args[3])
+    elseif isexpr(ex, :call)
+        push!(reactants, ex.args[1])
     else
         push!(reactants, underscorize(ex))
     end
diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl
index 2c566bb..a2e9ea2 100644
--- a/src/interface/reaction_parser.jl
+++ b/src/interface/reaction_parser.jl
@@ -3,7 +3,7 @@
 using MacroTools: postwalk
 
 struct FoldedReactant
-    species::Symbol
+    species::Union{Expr, Symbol}
     stoich::SampleableValues
     modality::Set{Symbol}
 end
@@ -71,6 +71,8 @@ function recursive_find_reactants!(
             4:length(ex.args),
         )
         recursive_find_reactants!(ex.args[3], mult, mods, reactants)
+    elseif isexpr(ex, :call)
+        push!(reactants, FoldedReactant(ex, mult, mods))
     else
         @error("malformed reaction")
     end
diff --git a/src/operators/equalize.jl b/src/operators/equalize.jl
index c8e5724..f52672f 100644
--- a/src/operators/equalize.jl
+++ b/src/operators/equalize.jl
@@ -1,4 +1,4 @@
-export @equalize
+export equalize!, @equalize
 
 expand_name_ff(ex) =
     if ex isa Expr && isexpr(ex, :macrocall)
@@ -53,9 +53,10 @@ function equalize!(acs::ReactionNetworkSchema, eqs = [])
     for attr in propertynames(acs.subparts)
         attr == :specName && continue
         attr_ = acs[:, attr]
-        for i = 1:length(attr_)
+        for i in eachindex(attr_)
             attr_[i] = escape_ref(attr_[i], collect(keys(specmap)))
             attr_[i] = recursively_substitute_vars!(specmap, attr_[i])
+            acs[i, attr] = attr_[i]
         end
     end
 
diff --git a/src/operators/joins.jl b/src/operators/joins.jl
index 5ac9808..c0183cb 100644
--- a/src/operators/joins.jl
+++ b/src/operators/joins.jl
@@ -1,24 +1,24 @@
 # model joins
-export @join
+export union_acs!, @join
 
 using MacroTools
 using MacroTools: prewalk
 
 """
-Merge `acs2` onto `acs1`, the attributes in `acs2` taking precedence. Identify respective species given `eqs`, renaming species in `acs2`.
+Merge `acs2` into `acs1`, the attributes in `acs2` taking precedence. Identify respective species given `eqs`, renaming species in `acs2`.
 """
-function union_acs!(acs1, acs2, name = gensym("acs_"), eqs = [])
+function union_acs!(acs1, acs2, name = gensym("acs"), eqs = [])
     acs2 = deepcopy(acs2)
     prepend!(acs2, name, eqs)
+
     for i in parts(acs2, :S)
         inc = incident(acs1, acs2[i, :specName], :specName)
-        isempty(inc) && (inc = add_part!(acs1, :S; specName = acs2[i, :specName]);
-        assign_defaults!(acs1))
-        return (acs1, acs2)
-        println(first(inc))
-        println(acs1[first(inc), :specModality])
-        println()
-        println(acs2[:, :specModality])
+
+        if isempty(inc)
+            inc = add_part!(acs1, :S; specName = acs2[i, :specName])
+            assign_defaults!(acs1)
+        end
+
         union!(acs1[first(inc), :specModality], acs2[i, :specModality])
 
         for attr in propertynames(acs1.subparts)
@@ -30,7 +30,9 @@ function union_acs!(acs1, acs2, name = gensym("acs_"), eqs = [])
     new_trans_ix = add_parts!(acs1, :T, nparts(acs2, :T))
     for attr in propertynames(acs2.subparts)
         !occursin("trans", string(attr)) && continue
-        acs1[new_trans_ix, attr] .= acs2[:, attr]
+        for (ix1, ix2) in enumerate(new_trans_ix)
+            acs1[ix2, attr] = acs2[ix1, attr]
+        end
     end
 
     foreach(
@@ -69,10 +71,10 @@ function prepend!(acs::ReactionNetworkSchema, name = gensym("acs"), eqs = [])
     for attr in propertynames(acs.subparts)
         attr == :specName && continue
         attr_ = acs[:, attr]
-        for i = 1:length(attr_)
+        for i in eachindex(attr_)
             attr_[i] = escape_ref(attr_[i], collect(keys(specmap)))
             attr_[i] = recursively_substitute_vars!(specmap, attr_[i])
-            attr_[i] isa Expr && (attr_[i] = prepend_obs(attr_[i], name))
+            acs[i, attr] = attr_[i]
         end
     end
 
diff --git a/src/solvers.jl b/src/solvers.jl
index 270e8c7..889ddd3 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -15,7 +15,7 @@ Compute resource requirements given transition quantities.
 """
 function get_reqs_init!(reqs, qs, state)
     reqs .= 0.0
-    for i = 1:size(reqs, 2)
+    for i in axes(reqs, 2)
         for tok in state[i, :transLHS]
             !any(m -> m in tok.modality, [:rate, :nonblock]) &&
                 (reqs[tok.index, i] += qs[i] * tok.stoich)
@@ -30,11 +30,14 @@ Compute resource requirements given transition quantities.
 """
 function get_reqs_ongoing!(reqs, qs, state)
     reqs .= 0.0
-    for i = 1:length(state.ongoing_transitions)
+    for i in eachindex(state.ongoing_transitions)
         for tok in state.ongoing_transitions[i][:transLHS]
             in(:rate, tok.modality) &&
                 (state.ongoing_transitions[i][:transCycleTime] > 0) &&
                 (reqs[tok.index, i] += qs[i] * tok.stoich * state.dt)
+            if in(:rate, tok.modality) && in(tok.species, state.structured_species)
+                error("Modality `:rate` is not supported for structured species in transition $(trans[:transName]).")
+            end
             in(:nonblock, tok.modality) && (reqs[tok.index, i] += qs[i] * tok.stoich)
         end
     end
@@ -55,7 +58,7 @@ end
 
 function alloc_weighted!(reqs, u, priorities, state)
     allocs = zero(reqs)
-    for i = 1:size(reqs, 1)
+    for i in axes(reqs, 1)
         s = sum(reqs[i, :])
         u[i] >= s && (allocs[i, :] .= reqs[i, :]; continue)
         foreach(j -> allocs[i, j] = reqs[i, j] * priorities[j], 1:size(reqs, 2))
@@ -69,7 +72,7 @@ end
 function alloc_greedy!(reqs, u, priorities, state)
     allocs = zero(reqs)
     sorted_trans = sort(1:size(reqs, 2); by = i -> -priorities[i])
-    for i = 1:size(reqs, 1)
+    for i in axes(reqs, 1)
         s = sum(reqs[i, :])
         u[i] >= s && (allocs[i, :] .= reqs[i, :]; continue)
         a = u[i]
@@ -97,12 +100,14 @@ function get_frac_satisfied(allocs, reqs, state)
     return qs
 end
 
+isinteger(x::Number) = x == trunc(x)
+
 """
 Given available allocations and qties of transitions requested to spawn, return number of spawned transitions. Update `alloc` to match actual allocation.
 """
 function get_init_satisfied(allocs, qs, state)
     reqs = zero(allocs)
-    for i = 1:size(allocs, 2)
+    for i in axes(allocs, 2)
         all(allocs[:, i] .>= 0) || (allocs[:, i] .= 0.0; qs[i] = 0)
         for tok in state[i, :transLHS]
             !any(m -> m in tok.modality, [:rate, :nonblock]) &&
@@ -120,6 +125,7 @@ function get_init_satisfied(allocs, qs, state)
     return qs
 end
 
+
 """
 Evolve transitions, spawn new transitions.
 """
@@ -165,16 +171,43 @@ function evolve!(state)
 
     # add spawned transitions to the heap
     for i in parts(state, :T)
-        qs[i] != 0 && push!(
-            state.ongoing_transitions,
-            Transition(
+        if qs[i] != 0
+            transition = Transition(
                 string(state[i, :transName]) * "_@$(state.t)",
+                i,
                 get_sampled_transition(state, i),
+                AbstractAlgebraicAgent[],
+                AbstractAlgebraicAgent[],
                 state.t,
                 qs[i],
                 0.0,
-            ),
-        )
+            )
+            push!(state.ongoing_transitions, transition)
+
+            bound = transition.bound_structured_agents
+
+            for (j, type) in enumerate(state.acs[:, :specName])
+                if type ∈ state.structured_species
+                    if !isinteger(allocs[j, i])
+                        error("For structured species, stoichiometry coefficient must be integer in transition $i.")
+                    end
+
+                    all_of_type = collect(values(inners(getagent(state, "structured/$(string(type))"))))
+                    filter!(!isblocked, all_of_type)
+                    sort!(all_of_type, by=a->priority(a, state.acs[i, :transName]), rev=true)
+
+                    ix = 1
+                    while allocs[j, i] > 0 && ix <= length(all_of_type)
+                        all_of_type[ix].bound_transition = transition
+                        push!(bound, all_of_type[ix])
+                        allocs[j, i] -= 1
+                        ix += 1
+                    end
+                end
+            end
+
+            context_eval(state, transition, state.wrap_fun(state.acs[i, :transPreAction]))
+        end
     end
 
     ## evolve ongoing transitions 
@@ -204,10 +237,33 @@ function evolve!(state)
     state.u .-= sum(allocs; dims = 2)
     actual_allocs .+= sum(allocs; dims = 2)
 
-    foreach(
-        i -> state.ongoing_transitions[i].state += qs[i] * state.dt,
-        eachindex(state.ongoing_transitions),
-    )
+    for i in eachindex(state.ongoing_transitions)
+        transition = state.ongoing_transitions[i]
+        if qs[i] != 0
+            transition.state += qs[i] * state.dt
+            bound = transition.nonblock_structured_agents
+
+            for (j, type) in enumerate(state.acs[:, :specName])
+                if type ∈ state.structured_species
+                    if !isinteger(allocs[j, i])
+                        error("For structured species, stoichiometry coefficient must be integer in transition $i.")
+                    end
+
+                    all_of_type = collect(values(inners(getagent(state, "structured/$(string(type))"))))
+                    filter!(!isblocked, all_of_type)
+                    sort!(all_of_type, by=a -> priority(a, state.acs[i, :transName]), rev=true)
+
+                    ix = 1
+                    while allocs[j, i] > 0 && ix <= length(all_of_type)
+                        all_of_type[ix].bound_transition = transition
+                        push!(bound, all_of_type[ix])
+                        allocs[j, i] -= 1
+                        ix += 1
+                    end
+                end
+            end
+        end
+    end
 
     push!(state.log, (:allocation, state.t, actual_allocs))
     return push!(
@@ -243,45 +299,74 @@ function finish!(state)
     while ix <= length(state.ongoing_transitions)
         trans_ = state.ongoing_transitions[ix]
         ((state.t - trans_.t) < trans_.trans[:transMaxLifeTime]) &&
-            trans_.state < trans_[:transCycleTime] &&
+            (trans_.state < trans_[:transCycleTime]) &&
             (ix += 1; continue)
-        toks_rhs = []
+        
+        q = if trans_.state >= trans_[:transCycleTime]
+            rand(Distributions.Binomial(Int(trans_.q), trans_[:transProbOfSuccess]))
+        else
+            0
+        end
+
         for r in extract_reactants(trans_[:transRHS], state)
-            i = find_index(r.species, state)
-            push!(
-                toks_rhs,
-                UnfoldedReactant(
-                    i,
-                    r.species,
-                    context_eval(state, state.wrap_fun(r.stoich)),
-                    r.modality ∪ state[i, :specModality],
-                ),
-            )
+            if r.species isa Expr
+                if !(r.species.args[1] ∈ state.structured_species)
+                    error("Unresolved structured species species $(r.species.args[1]).")
+                end
+
+                i = find_index(r.species.args[1], state)
+                stoich = context_eval(state, trans_, state.wrap_fun(r.stoich))
+
+                state.u[i] += q * stoich
+                val_reward += state[i, :specReward] * q * stoich
+
+                for _ in 1:q
+                    a = context_eval(state, trans_, state.wrap_fun(r.species))
+                    entangle!(getagent(state, "structured/$(r.species.args[1])"), a)
+                end
+            else
+                i = find_index(r.species, state)
+                stoich = context_eval(state, trans_, state.wrap_fun(r.stoich))
+
+                state.u[i] += q * stoich
+                val_reward += state[i, :specReward] * q * stoich
+            end
         end
+
         for tok in trans_[:transLHS]
-            in(:conserved, tok.modality) && (
+            if in(:conserved, tok.modality)
                 state.u[tok.index] +=
                     trans_.q *
                     tok.stoich *
                     (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1)
-            )
-        end
-
-        q = if trans_.state >= trans_[:transCycleTime]
-            rand(Distributions.Binomial(Int(trans_.q), trans_[:transProbOfSuccess]))
-        else
-            0
+                if tok.species ∈ state.structured_species
+                    for _ in 1:(trans_.q * tok.stoich)
+                        trans_.bound_structured_agents[begin].bound_transition = nothing
+                        deleteat!(trans_.bound_structured_agents, 1)
+                    end
+                end
+            end
+
+            if in(:nonblock, tok.modality)
+                if in(:conserved, tok.modality)
+                    error("Modalities `:conserved` and `:nonblock` cannot be specified at the same time.")
+                end
+
+                state.u[tok.index] += trans_.q * tok.stoich 
+                if tok.species ∈ state.structured_species
+                    for _ in 1:(trans_.q * tok.stoich)
+                        trans_.nonblock_structured_agents[begin].bound_transition = nothing
+                        deleteat!(trans_.nonblock_structured_agents, 1)
+                    end
+                end
+            end
         end
 
-        foreach(
-            tok -> (state.u[tok.index] += q * tok.stoich;
-            val_reward += state[tok.index, :specReward] * q * tok.stoich),
-            toks_rhs,
-        )
+        context_eval(state, trans_, state.wrap_fun(state.acs[trans_.i, :transPostAction]))
 
-        context_eval(state, trans_.trans[:transPostAction])
         terminated_all[Symbol(trans_[:transHash])] =
             get(terminated_all, Symbol(trans_[:transHash]), 0) + trans_.q
+        
         terminated_success[Symbol(trans_[:transHash])] =
             get(terminated_success, Symbol(trans_[:transHash]), 0) + q
 
@@ -301,6 +386,14 @@ function free_blocked_species!(state)
     for trans in state.ongoing_transitions, tok in trans[:transLHS]
         in(:nonblock, tok.modality) && (state.u[tok.index] += q * tok.stoich)
     end
+
+    for trans in state.ongoing_transitions
+        for a in trans.nonblock_structured_agents
+            a.bound_transition = nothing
+        end
+
+        empty!(trans.nonblock_structured_agents)
+    end
 end
 
 ## resolve tspan, tstep
@@ -319,7 +412,7 @@ function ReactionNetworkProblem(
     acs::ReactionNetworkSchema,
     u0 = Dict(),
     p = Dict();
-    name = "reactive_network",
+    name = "reaction_network",
     kwargs...,
 )
     assign_defaults!(acs)
@@ -333,7 +426,10 @@ function ReactionNetworkProblem(
     keywords[:tspan], keywords[:tstep] = get_tcontrol(keywords[:tspan], keywords)
 
     acs = remove_choose(acs)
-    attrs, transitions, wrap_fun = compile_attrs(acs)
+
+    structured_species_names = acs[filter(i -> acs[i, :specStructured], 1:nparts(acs, :S)), :specName]
+
+    attrs, transitions, wrap_fun = compile_attrs(acs, structured_species_names)
     transition_recipes = transitions
     u0_init = zeros(nparts(acs, :S))
 
@@ -374,6 +470,7 @@ function ReactionNetworkProblem(
         u0_init,
         merge(p, Dict(:strategy => get(keywords, :alloc_strategy, :weighted))),
         keywords[:tspan][1],
+        Symbol[],
         keywords[:tspan],
         get(keywords, :tstep, 1),
         transitions,
@@ -384,6 +481,14 @@ function ReactionNetworkProblem(
         sol,
     )
 
+    entangle!(network, FreeAgent("structured"))
+
+    structured_species = filter(i -> acs[i, :specStructured], 1:nparts(acs, :S))
+    for i in structured_species
+        push!(network.structured_species, acs[i, :specName])
+        entangle!(getagent(network, "structured"), FreeAgent(string(acs[i, :specName])))
+    end
+
     save!(network)
 
     return network
@@ -400,12 +505,20 @@ function AlgebraicAgents._reinit!(state::ReactionNetworkProblem)
     return state
 end
 
+function update_u_structured!(state)
+    for (i, type) in enumerate(state.acs[:, :specName])
+        structured_agents_type = values(inners(getagent(state, "structured/$type")))
+        state.u[i] = count(!isblocked, structured_agents_type)
+    end
+end
+
 function AlgebraicAgents._step!(state::ReactionNetworkProblem)
     free_blocked_species!(state)
     update_observables(state)
     sample_transitions!(state)
     evolve!(state)
     finish!(state)
+
     event_action!(state)
 
     push!(
diff --git a/src/state.jl b/src/state.jl
index 97e85db..70ed436 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -12,14 +12,20 @@ end
 Ongoing transition auxiliary structure.
 """
 @aagent struct Transition
+    i::Int
+
     trans::Dict{Symbol,Any}
 
+    bound_structured_agents::Vector{AbstractAlgebraicAgent}
+    nonblock_structured_agents::Vector{AbstractAlgebraicAgent}
+
     t::Float64
     q::Float64
     state::Float64
 end
 
 Base.getindex(state::Transition, key) = state.trans[key]
+Base.setindex!(state::Transition, val, key) = state.trans[key] = val
 
 @aagent struct Observable
     last::Float64 # last sampling time
@@ -40,6 +46,8 @@ end
     p::Any
     t::Float64
 
+    structured_species::Vector{Symbol}
+
     tspan::Tuple{Float64,Float64}
     dt::Float64
 
@@ -55,17 +63,21 @@ end
 
 # get value of a numeric expression
 # evaluate compiled numeric expression in context of (u, p, t)
-function context_eval(state::ReactionNetworkProblem, o)
-    o = o isa Function ? Base.invokelatest(o, state) : o
-
+function context_eval(state::ReactionNetworkProblem, transition, o)
+    o = o isa Function ? Base.invokelatest(o, state, transition) : o
+    
     return o isa Sampleable ? rand(o) : o
 end
 
 function Base.getindex(state::ReactionNetworkProblem, keys...)
-    return context_eval(
-        state,
-        (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]],
-    )
+    if any(occursin.(["transPreAction", "transPostAction"], Ref(string(keys[2]))))
+        return state.acs[keys[1], keys[2]]
+    else
+        return context_eval(
+            state, nothing,
+            (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]],
+        )
+    end
 end
 
 function init_u!(state::ReactionNetworkProblem)
@@ -120,7 +132,7 @@ function resample!(state::ReactionNetworkProblem, o::Observable)
     o.last = state.t
     isempty(o.range) && (return o.val = missing)
 
-    return o.sampled = context_eval(state, sample_range(o.range, state))
+    return o.sampled = context_eval(state, nothing, sample_range(o.range, state))
 end
 
 resample(state::ReactionNetworkProblem, o::Symbol) = resample!(state, state.observables[o])
@@ -164,10 +176,10 @@ function sample_transitions!(state::ReactionNetworkProblem)
         l_line, r_line = prune_r_line(state.transition_recipes[:trans][i])
 
         for attr in keys(state.transition_recipes)
-            attr ∈ [:trans, :transPostAction, :transActivated, :transHash] && continue
+            (attr ∈ [:trans, :transPreAction, :transPostAction, :transActivated, :transHash]) && continue
             push!(
                 state.transitions[attr],
-                context_eval(state, state.transition_recipes[attr][i]),
+                context_eval(state, nothing, state.transition_recipes[attr][i]),
             )
         end
 
@@ -179,17 +191,20 @@ function sample_transitions!(state::ReactionNetworkProblem)
                 UnfoldedReactant(
                     j,
                     r.species,
-                    context_eval(state, state.wrap_fun(r.stoich)),
+                    context_eval(state, nothing, state.wrap_fun(r.stoich)),
                     r.modality ∪ state[j, :specModality],
                 ),
             )
         end
+        
         push!(state.transitions[:transLHS], reactants)
         push!(state.transitions[:transRHS], r_line)
+        
         foreach(
             k -> push!(state.transitions[k], state.transition_recipes[k][i]),
-            [:transPostAction, :transToSpawn, :transHash],
+            [:transPreAction, :transPostAction, :transToSpawn, :transHash],
         )
+
         state.transition_recipes[:transToSpawn] .= 0
     end
 end
diff --git a/tutorial/agents-integration/Project.toml b/tutorial/agents-integration/Project.toml
new file mode 100644
index 0000000..ef794fc
--- /dev/null
+++ b/tutorial/agents-integration/Project.toml
@@ -0,0 +1,9 @@
+[deps]
+AlgebraicAgents = "f6eb0ae3-10fa-40e6-88dd-9006ba45093a"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
+JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
+MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
+PlotGraphviz = "78a92bc3-407c-4e2f-aae5-75bb47a6fe36"
+ReactiveDynamics = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4"
+SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622"
diff --git a/tutorial/agents-integration/agents.jl b/tutorial/agents-integration/agents.jl
new file mode 100644
index 0000000..c4304b9
--- /dev/null
+++ b/tutorial/agents-integration/agents.jl
@@ -0,0 +1,101 @@
+# --------------------------------------------------------------------------------
+# Structured (agent-based) species
+
+#=
+import Pkg
+Pkg.activate(".")
+Pkg.dev("../..")
+Pkg.add(["AlgebraicAgents"])
+=#
+
+using ReactiveDynamics
+using AlgebraicAgents
+
+# Define the "symbolic" reaction network.
+network = @ReactionNetworkSchema
+
+# Below, we combine
+# - "classical species" (continuous or discrete; considered as pure quantities);
+# - "structured agents" (possibly with custom evolutionary function; these can appear both on LHS and RHS).
+
+@push network begin
+    # With specified intensities, generate experimental resources.
+    ρ1, ∅ --> R1
+    ρ2, ∅ --> R2
+    
+    # Generate "Molecule 1" (where the integer corresponds to a "state" of, e.g., experimental triage).
+    ρ3, ∅ --> M1(@t(), rand(4))
+    
+    # Based on properties of particular "structured agent" assigned to the transition,
+    # we can update the attributes of the instance of a transition (such as probability of success).
+    
+    # Transition "Molecule 1" into "Molecule 2."
+    # Update transition probability based on properties of "M1," 
+    # which was assigned as a "resource" to the transition.
+    ρ4, R1 + M1 --> M2(@t(), rand(4)), preAction => update_prob_transition(state, transition)
+end
+
+@prob_init network R1 = 10 R2 = 15
+
+# As for structured agents, we will need to instantiate the instances
+# and add them to the instance of a network. But first, we still need to define these types.
+@prob_init network M1 = 2 M2 = 0
+
+@prob_params network ρ1 = 2 ρ2 = 1 ρ3 = 3 ρ4 = 4
+
+@prob_meta network tspan = 100 dt = 1.0
+
+# We use `@structured` macro, which is a convenience wrapper around `@aagent`),
+# defined in ReactiveDynamics.jl
+@structured network struct M1
+    descriptor
+    time_created
+end
+
+using Random
+
+# Type `M1` lives in the scope of ReactiveDynamics.
+# Accordingly, we have to explicitly declare the scope.
+using ReactiveDynamics: M1
+
+ReactiveDynamics.M1(time, descriptor) = M1("M1" * randstring(4), nothing, descriptor, time)
+
+# We define the function which updates the transition probability.
+# This has to be accessible from within the name scope of ReactiveDynamics.
+@register begin
+    update_prob_transition = function (state, transition)
+        if !isnothing(transition) && !isempty(transition.bound_structured_agents)
+            bound_agent = first(transition.bound_structured_agents)
+            
+            transition[:transProbOfSuccess] = min(1.0, sum(bound_agent.descriptor))
+        end
+    end
+end
+
+# Alternatively, we can define a structured agent type using
+# the usual `@aagent` macro. This must be evaluated inside the scope
+# of ReactiveDynamics.
+@register begin
+    @aagent BaseStructuredSpecies AbstractStructuredSpecies struct M2
+        descriptor
+        time_created
+    end
+
+    using Random
+    M2(time, descriptor) = M2("M2" * randstring(4), nothing, descriptor, time)
+end
+
+# Let the network know that the species is structured.
+ReactiveDynamics.register_structured_species!(network, :M2)
+
+# --------------------------------------------------------------------------------
+# Instantiate the network.
+network_instance = ReactionNetworkProblem(network)
+
+for i in 1:2
+    add_structured_species!(network_instance, ReactiveDynamics.M1(0.0, rand(4)))
+end
+
+# --------------------------------------------------------------------------------
+# Simulate the network.
+simulate(network_instance, 10)
\ No newline at end of file
diff --git a/tutorial/agents-integration/agents_integration.jl b/tutorial/agents-integration/agents_integration.jl
new file mode 100644
index 0000000..25fb6fd
--- /dev/null
+++ b/tutorial/agents-integration/agents_integration.jl
@@ -0,0 +1,302 @@
+import Pkg
+
+# Manually add dependencies
+#=
+Pkg.activate(".")
+Pkg.develop(path = "../..")
+Pkg.add(["MacroTools", "AlgebraicAgents", "JSON3", "Distributions"])
+=#
+
+# --------------------------------------------------------------------------------
+# Parametrize the reaction network
+
+# We will have two classes of entities, each of which can be in a number of different states.
+# States "S" and "F" will internally denote some terminal, "success" and "failure" states, respectively.
+
+terminal_states = ["S", "F"]
+M1_states = "M1_" .* ["A", "B", "C", "D", terminal_states...]
+M2_states = "M2_" .* ["A", "B", "C", "D", "E", terminal_states...]
+
+experimental_resources = ["ER1", "ER2", "ER3"]
+
+M1_transition_probs = round.(rand(length(M1_states), length(M1_states)); digits=2)
+M2_transition_probs = round.(rand(length(M2_states), length(M2_states)); digits=2)
+
+# Ensure forward flow between states and set zero transition prob to terminal states
+for probs in [M1_transition_probs, M2_transition_probs]
+    for i in axes(probs, 1)
+        probs[i, i:end] .= 0
+    end
+
+    probs[:, end-1] .= 0
+end
+
+M1_priorities = round.(rand(length(M1_states)); digits=2)
+M2_priorities = 2 * round.(rand(length(M2_states)); digits=2)
+
+M1_resources = [rand(1:5, length(experimental_resources)) for _ in M1_states]
+M2_resources = [2 * rand(1:5, length(experimental_resources)) for _ in M2_states]
+
+M1_durations = rand(2:5, length(M1_states))
+M2_durations = rand(2:5, length(M2_states))
+
+# --------------------------------------------------------------------------------
+# Add initial quantities
+
+M1_initial = [rand(1:10, length(M1_states)-2)..., 0, 0]
+M2_initial = [rand(1:10, length(M2_states)-2)..., 0, 0]
+
+experimental_resources_initial = rand(1e2:3e2, size(experimental_resources))
+
+# --------------------------------------------------------------------------------
+# Export to JSON
+
+# First, we build a dictionary.
+data = Dict("species" => [], "transitions" => [])
+
+species, transitions = data["species"], data["transitions"]
+
+# --------------------------------------------------------------------------------
+# Add species with initial quantities.
+# Later, we may add a couple more attributes (conserved resources, etc.).
+
+for (state, q) in zip(M1_states, M1_initial)
+    push!(species, Dict("name" => "$state", "initial" => q))
+end
+
+for (state, q) in zip(M2_states, M2_initial)
+    push!(species, Dict("name" => "$state", "initial" => q))
+end
+
+for (res, q) in zip(experimental_resources, experimental_resources_initial)
+    push!(species, Dict("name" => "$res", "initial" => q))
+end
+
+# --------------------------------------------------------------------------------
+# Add transitions for entity class M1.
+
+for (i, state) in enumerate(M1_states[begin:end-2])
+    t = Dict{String, Any}("priority" => M1_priorities[i], "duration" => M1_durations[i], "rate" => state, "name" => "M1")
+
+    # from (left-hand side)
+    push!(t, "from" => [Dict("name" => res, "q" => q) for (res, q) in zip(experimental_resources, M1_resources[i])])
+    push!(t["from"], Dict("name" => state, "q" => 1))
+
+    # to (right-hand side)
+    push!(t, "to" => [])
+    to = t["to"]
+
+    for (j, state2) in enumerate(M1_states[i:end])
+        if M1_transition_probs[j+i-1, i] > 0
+            push!(to, Dict("probability" => M1_transition_probs[j+i-1, i], "reactants" => [Dict("name" => state2, "q" => 1)]))
+        end
+    end
+
+    push!(transitions, t)
+end
+
+# Add transitions for entity class M2.
+
+for (i, state) in enumerate(M2_states[begin:end-2])
+    t = Dict{String, Any}("priority" => M2_priorities[i], "duration" => M2_durations[i], "rate" => state, "name" => "M2")
+
+    # from (left-hand side)
+    push!(t, "from" => [Dict("name" => res, "q" => q) for (res, q) in zip(experimental_resources, M2_resources[i])])
+    push!(t["from"], Dict("name" => state, "q" => 1))
+
+    # to (right-hand side)
+    push!(t, "to" => [])
+    to = t["to"]
+
+    for (j, state2) in enumerate(M2_states[i:end])
+        if M2_transition_probs[j+i-1, i] > 0
+            push!(to, Dict("probability" => M2_transition_probs[j+i-1, i], "reactants" => [Dict("name" => state2, "q" => 1)]))
+        end
+    end
+
+    push!(transitions, t)
+end
+
+using JSON3
+open("reaction_network.json", "w") do io
+    #JSON3.pretty(io, data)
+end
+
+# --------------------------------------------------------------------------------
+# Import from JSON
+# Eventually move into the module (refactor current CSV interface).
+
+# Species and initial values.
+name = "reaction_network"
+str_init = "@prob_init $name"
+for s in data["species"]
+    str_init *= " $(s["name"])=$(s["initial"])"
+end
+
+function get_subline(d::Vector)
+    if isempty(d)
+        return "∅"
+    elseif haskey(first(d), "probability")
+        sublines = ["($(sd["probability"]), " * join(["$(s["q"]) * $(s["name"])" for s in sd["reactants"]], " + ") * ")" for sd in d]
+        return "@choose(" * join(sublines, ", ") * ")"
+    else
+        return join(["$(s["q"]) * $(s["name"])" for s in d], " + ")
+    end
+end
+
+# Transitions.
+str_transitions = []
+for t in data["transitions"]
+    line = t["rate"] * ", " * get_subline(t["from"]) * " --> " * get_subline(t["to"])
+    line *= ", name => $(t["name"]), priority => $(t["priority"]), cycletime => $(t["duration"])"
+    push!(str_transitions, line)
+end
+
+push!(str_transitions, "i1, ∅ --> M1_A, name => M1_creation")
+push!(str_transitions, "i2, ∅ --> M2_A, name => M2_creation")
+
+str_params = "@prob_params $name i1 = .3 i2 = .2"
+
+str_network_def = """
+    begin
+        $name = @ReactionNetworkSchema
+        @push $name begin
+            $(join(str_transitions, '\n'))
+        end
+        $str_init
+        $str_params
+    end
+"""
+
+using MacroTools: striplines
+expr_network_def = striplines(Meta.parseall(str_network_def))
+
+using ReactiveDynamics
+
+eval(expr_network_def)
+
+@isdefined reaction_network
+
+# --------------------------------------------------------------------------------
+# Solve problem
+
+@prob_meta reaction_network tspan = 100 dt = 1.0
+
+# Convert network into an AlgAgents hierarchy.
+problem = ReactionNetworkProblem(reaction_network; name = "network")
+
+# AlgAgents: "periodic" callback.
+using AlgebraicAgents
+
+@aagent struct Controller
+    M1_λ::Float64
+    M2_λ::Float64
+
+    log::Vector{String}
+
+    current_time::Float64
+    time_step::Float64
+
+    initial_time::Float64
+end
+
+function Controller(name::String, M1_λ::Float64, M2_λ::Float64, time::T, time_step::T) where {T<:Real}
+    return Controller(name, M1_λ, M2_λ, String[], time, time_step, time)
+end
+
+using Distributions: Poisson
+
+function AlgebraicAgents._step!(c::Controller)
+    n_removed_M1 = rand(Poisson(c.time_step * c.M1_λ))
+    n_removed_M2 = rand(Poisson(c.time_step * (c.M2_λ + c.M1_λ)))
+
+    transitions = getagent(c, "../network").ongoing_transitions
+    M1_transitions = filter(x -> x[:transName] === :M1, transitions)
+    M2_transitions = filter(x -> x[:transName] === :M2, transitions)
+
+    M1_transitions_delete = isempty(M1_transitions) ? [] : unique(rand(M1_transitions, n_removed_M1))
+    for trans in M1_transitions_delete
+        trans.state = trans[:transCycleTime]
+        trans.trans[:transProbOfSuccess] = 0
+    end
+
+    M2_transitions_delete = isempty(M2_transitions) ? [] : unique(rand(M2_transitions, n_removed_M2))
+    for trans in M2_transitions_delete
+        trans.state = trans[:transCycleTime]
+        trans.trans[:transProbOfSuccess] = 0
+    end
+
+    push!(c.log, "t = $(c.current_time) removed compounds: " * join(getname.(union(M1_transitions_delete, M2_transitions_delete)), ", "))
+
+    return c.current_time += c.time_step
+end
+
+AlgebraicAgents._projected_to(c::Controller) = c.current_time
+
+c = Controller("controller", 1e-1, 2e-1, 0., 1.);
+
+compound_problem = ⊕(problem, c; name = "compound problem");
+
+# Simulate
+simulate(compound_problem, 100);
+
+# Access solution
+compound_problem.inners["network"].sol
+compound_problem.inners["controller"].log
+
+# Plot solution
+draw(compound_problem.inners["network"])
+
+# --------------------------------------------------------------------------------
+# Add resource making part to the reaction network
+
+eval(expr_network_def)
+
+n_primary_resources = 5
+primary_resources = ["R$i" for i in 1:n_primary_resources]
+
+str_resource_making_transitions = [
+    "p_primary_$i, ∅ --> $res, name => primary_resource_maker_$i" for (i, res) in enumerate(primary_resources)
+] 
+
+for (i, res) in enumerate(experimental_resources)
+    stoich = rand(1:5, n_primary_resources)
+    rate = "p_$i * (" * join(primary_resources, " + ") * ")"
+    transition_lhs = join(["$(stoich[i])" * "$primary_res" for (i, primary_res) in enumerate(primary_resources)], " + ")
+    
+    push!(str_resource_making_transitions, "$rate, $transition_lhs --> $res, name => resource_maker_$i")
+end
+
+str_resource_making_params = 
+    "@prob_params resource_making_network " *
+    join(["p_$i = $(rand(1:3))" for i in 1:length(experimental_resources)], " ") * " " *
+    join(["p_primary_$i = $(rand(1:5))" for i in 1:length(primary_resources)], " ")
+
+str_network_def = """
+    begin
+        resource_making_network = @ReactionNetworkSchema
+        @push resource_making_network begin
+            $(join(str_resource_making_transitions, '\n'))
+        end
+        $str_resource_making_params
+    end
+"""
+
+expr_network_resource_making_def = striplines(Meta.parseall(str_network_def))
+
+eval(expr_network_resource_making_def)
+
+extended_network = union_acs!(reaction_network, resource_making_network, "resource_making_network")
+
+#equalize!(extended_network, [Meta.parseall("$res=resource_making_network.$res") for res in experimental_resources])
+str_equalize = "@equalize extended_network " * join(["$res=resource_making_network.$res" for res in experimental_resources], " ")
+
+equalize_expr = striplines(Meta.parseall(str_equalize))
+eval(equalize_expr)
+
+@prob_meta extended_network tspan = 100 dt = 1.0
+
+# Convert network into an AlgAgents hierarchy.
+extended_problem = ReactionNetworkProblem(extended_network; name = "extended_network")
+
+simulate(extended_problem, 100);
\ No newline at end of file

From 2814b14e497f9a8ac2d867a83b2a8348df6f3c9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= 
Date: Thu, 22 Feb 2024 19:53:24 +0100
Subject: [PATCH 09/11] Format

---
 src/ReactiveDynamics.jl                       |   2 +-
 src/compilers.jl                              |   2 +-
 src/interface/agents.jl                       |  17 ++-
 src/interface/reaction_parser.jl              |   2 +-
 src/solvers.jl                                |  52 +++++---
 src/state.jl                                  |  14 +-
 tutorial/agents-integration/agents.jl         |  24 ++--
 .../agents-integration/agents_integration.jl  | 124 +++++++++++++-----
 8 files changed, 166 insertions(+), 71 deletions(-)

diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl
index a7192bf..c10b7a0 100644
--- a/src/ReactiveDynamics.jl
+++ b/src/ReactiveDynamics.jl
@@ -84,7 +84,7 @@ const ReactionNetworkSchema = FoldedReactionNetworkType{
     Set{Symbol},
     FoldedObservable,
     Any,
-    Bool
+    Bool,
 }
 
 Base.convert(::Type{Symbol}, ex::String) = Symbol(ex)
diff --git a/src/compilers.jl b/src/compilers.jl
index 0c741d8..b4faba8 100644
--- a/src/compilers.jl
+++ b/src/compilers.jl
@@ -118,7 +118,7 @@ function wrap_expr(fex, species_names, prm_names, varmap)
 
     return eval(quote
         function (state, transition)
-            $letex
+            return $letex
         end
     end)
 end
diff --git a/src/interface/agents.jl b/src/interface/agents.jl
index 6233f63..842a7f8 100644
--- a/src/interface/agents.jl
+++ b/src/interface/agents.jl
@@ -9,7 +9,7 @@ abstract type AbstractStructuredSpecies <: AbstractAlgebraicAgent end
 # In general, we will probably assume that each "structured agent" type implements this field.
 # Otherwise, it would be possible to implement getter and setter interface and use it from within ReaDyn.
 @aagent FreeAgent struct BaseStructuredSpecies
-    bound_transition::Union{Nothing, ReactiveDynamics.Transition}
+    bound_transition::Union{Nothing,ReactiveDynamics.Transition}
 end
 
 # We use this to let the network know that the type is structured.
@@ -27,16 +27,21 @@ end
 # Convenience macro to define structured species.
 macro structured(network, type)
     name = Docs.namify(type.args[2])
-    
-    quote       
-        $(AlgebraicAgents.aagent(BaseStructuredSpecies, AbstractStructuredSpecies, type, ReactiveDynamics))
+
+    quote
+        $(AlgebraicAgents.aagent(
+            BaseStructuredSpecies,
+            AbstractStructuredSpecies,
+            type,
+            ReactiveDynamics,
+        ))
         register_structured_species!($(esc(network)), $(QuoteNode(name)))
     end
 end
 
 # Add a structured agent instance to an instance of a reaction network.
 function add_structured_species!(problem::ReactionNetworkProblem, agent)
-    entangle!(getagent(problem, "structured/$(nameof(typeof(agent)))"), agent)
+    return entangle!(getagent(problem, "structured/$(nameof(typeof(agent)))"), agent)
 end
 
 import AlgebraicAgents
@@ -49,4 +54,4 @@ AlgebraicAgents._step!(::AbstractStructuredSpecies) = nothing
 isblocked(a) = !isnothing(a.bound_transition)
 
 # Priority with which an unbound agent will be assigned to a transition.
-priority(a, transition) = 0.0
\ No newline at end of file
+priority(a, transition) = 0.0
diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl
index a2e9ea2..8ad0651 100644
--- a/src/interface/reaction_parser.jl
+++ b/src/interface/reaction_parser.jl
@@ -3,7 +3,7 @@
 using MacroTools: postwalk
 
 struct FoldedReactant
-    species::Union{Expr, Symbol}
+    species::Union{Expr,Symbol}
     stoich::SampleableValues
     modality::Set{Symbol}
 end
diff --git a/src/solvers.jl b/src/solvers.jl
index 889ddd3..cb86797 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -36,7 +36,9 @@ function get_reqs_ongoing!(reqs, qs, state)
                 (state.ongoing_transitions[i][:transCycleTime] > 0) &&
                 (reqs[tok.index, i] += qs[i] * tok.stoich * state.dt)
             if in(:rate, tok.modality) && in(tok.species, state.structured_species)
-                error("Modality `:rate` is not supported for structured species in transition $(trans[:transName]).")
+                error(
+                    "Modality `:rate` is not supported for structured species in transition $(trans[:transName]).",
+                )
             end
             in(:nonblock, tok.modality) && (reqs[tok.index, i] += qs[i] * tok.stoich)
         end
@@ -125,7 +127,6 @@ function get_init_satisfied(allocs, qs, state)
     return qs
 end
 
-
 """
 Evolve transitions, spawn new transitions.
 """
@@ -189,12 +190,20 @@ function evolve!(state)
             for (j, type) in enumerate(state.acs[:, :specName])
                 if type ∈ state.structured_species
                     if !isinteger(allocs[j, i])
-                        error("For structured species, stoichiometry coefficient must be integer in transition $i.")
+                        error(
+                            "For structured species, stoichiometry coefficient must be integer in transition $i.",
+                        )
                     end
 
-                    all_of_type = collect(values(inners(getagent(state, "structured/$(string(type))"))))
+                    all_of_type = collect(
+                        values(inners(getagent(state, "structured/$(string(type))"))),
+                    )
                     filter!(!isblocked, all_of_type)
-                    sort!(all_of_type, by=a->priority(a, state.acs[i, :transName]), rev=true)
+                    sort!(
+                        all_of_type;
+                        by = a -> priority(a, state.acs[i, :transName]),
+                        rev = true,
+                    )
 
                     ix = 1
                     while allocs[j, i] > 0 && ix <= length(all_of_type)
@@ -246,12 +255,20 @@ function evolve!(state)
             for (j, type) in enumerate(state.acs[:, :specName])
                 if type ∈ state.structured_species
                     if !isinteger(allocs[j, i])
-                        error("For structured species, stoichiometry coefficient must be integer in transition $i.")
+                        error(
+                            "For structured species, stoichiometry coefficient must be integer in transition $i.",
+                        )
                     end
 
-                    all_of_type = collect(values(inners(getagent(state, "structured/$(string(type))"))))
+                    all_of_type = collect(
+                        values(inners(getagent(state, "structured/$(string(type))"))),
+                    )
                     filter!(!isblocked, all_of_type)
-                    sort!(all_of_type, by=a -> priority(a, state.acs[i, :transName]), rev=true)
+                    sort!(
+                        all_of_type;
+                        by = a -> priority(a, state.acs[i, :transName]),
+                        rev = true,
+                    )
 
                     ix = 1
                     while allocs[j, i] > 0 && ix <= length(all_of_type)
@@ -301,7 +318,7 @@ function finish!(state)
         ((state.t - trans_.t) < trans_.trans[:transMaxLifeTime]) &&
             (trans_.state < trans_[:transCycleTime]) &&
             (ix += 1; continue)
-        
+
         q = if trans_.state >= trans_[:transCycleTime]
             rand(Distributions.Binomial(Int(trans_.q), trans_[:transProbOfSuccess]))
         else
@@ -320,7 +337,7 @@ function finish!(state)
                 state.u[i] += q * stoich
                 val_reward += state[i, :specReward] * q * stoich
 
-                for _ in 1:q
+                for _ = 1:q
                     a = context_eval(state, trans_, state.wrap_fun(r.species))
                     entangle!(getagent(state, "structured/$(r.species.args[1])"), a)
                 end
@@ -340,7 +357,7 @@ function finish!(state)
                     tok.stoich *
                     (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1)
                 if tok.species ∈ state.structured_species
-                    for _ in 1:(trans_.q * tok.stoich)
+                    for _ = 1:(trans_.q*tok.stoich)
                         trans_.bound_structured_agents[begin].bound_transition = nothing
                         deleteat!(trans_.bound_structured_agents, 1)
                     end
@@ -349,12 +366,14 @@ function finish!(state)
 
             if in(:nonblock, tok.modality)
                 if in(:conserved, tok.modality)
-                    error("Modalities `:conserved` and `:nonblock` cannot be specified at the same time.")
+                    error(
+                        "Modalities `:conserved` and `:nonblock` cannot be specified at the same time.",
+                    )
                 end
 
-                state.u[tok.index] += trans_.q * tok.stoich 
+                state.u[tok.index] += trans_.q * tok.stoich
                 if tok.species ∈ state.structured_species
-                    for _ in 1:(trans_.q * tok.stoich)
+                    for _ = 1:(trans_.q*tok.stoich)
                         trans_.nonblock_structured_agents[begin].bound_transition = nothing
                         deleteat!(trans_.nonblock_structured_agents, 1)
                     end
@@ -366,7 +385,7 @@ function finish!(state)
 
         terminated_all[Symbol(trans_[:transHash])] =
             get(terminated_all, Symbol(trans_[:transHash]), 0) + trans_.q
-        
+
         terminated_success[Symbol(trans_[:transHash])] =
             get(terminated_success, Symbol(trans_[:transHash]), 0) + q
 
@@ -427,7 +446,8 @@ function ReactionNetworkProblem(
 
     acs = remove_choose(acs)
 
-    structured_species_names = acs[filter(i -> acs[i, :specStructured], 1:nparts(acs, :S)), :specName]
+    structured_species_names =
+        acs[filter(i -> acs[i, :specStructured], 1:nparts(acs, :S)), :specName]
 
     attrs, transitions, wrap_fun = compile_attrs(acs, structured_species_names)
     transition_recipes = transitions
diff --git a/src/state.jl b/src/state.jl
index 70ed436..ed7a7f9 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -65,7 +65,7 @@ end
 # evaluate compiled numeric expression in context of (u, p, t)
 function context_eval(state::ReactionNetworkProblem, transition, o)
     o = o isa Function ? Base.invokelatest(o, state, transition) : o
-    
+
     return o isa Sampleable ? rand(o) : o
 end
 
@@ -74,7 +74,8 @@ function Base.getindex(state::ReactionNetworkProblem, keys...)
         return state.acs[keys[1], keys[2]]
     else
         return context_eval(
-            state, nothing,
+            state,
+            nothing,
             (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]],
         )
     end
@@ -176,7 +177,10 @@ function sample_transitions!(state::ReactionNetworkProblem)
         l_line, r_line = prune_r_line(state.transition_recipes[:trans][i])
 
         for attr in keys(state.transition_recipes)
-            (attr ∈ [:trans, :transPreAction, :transPostAction, :transActivated, :transHash]) && continue
+            (
+                attr ∈
+                [:trans, :transPreAction, :transPostAction, :transActivated, :transHash]
+            ) && continue
             push!(
                 state.transitions[attr],
                 context_eval(state, nothing, state.transition_recipes[attr][i]),
@@ -196,10 +200,10 @@ function sample_transitions!(state::ReactionNetworkProblem)
                 ),
             )
         end
-        
+
         push!(state.transitions[:transLHS], reactants)
         push!(state.transitions[:transRHS], r_line)
-        
+
         foreach(
             k -> push!(state.transitions[k], state.transition_recipes[k][i]),
             [:transPreAction, :transPostAction, :transToSpawn, :transHash],
diff --git a/tutorial/agents-integration/agents.jl b/tutorial/agents-integration/agents.jl
index c4304b9..5300f0d 100644
--- a/tutorial/agents-integration/agents.jl
+++ b/tutorial/agents-integration/agents.jl
@@ -22,17 +22,19 @@ network = @ReactionNetworkSchema
     # With specified intensities, generate experimental resources.
     ρ1, ∅ --> R1
     ρ2, ∅ --> R2
-    
+
     # Generate "Molecule 1" (where the integer corresponds to a "state" of, e.g., experimental triage).
     ρ3, ∅ --> M1(@t(), rand(4))
-    
+
     # Based on properties of particular "structured agent" assigned to the transition,
     # we can update the attributes of the instance of a transition (such as probability of success).
-    
+
     # Transition "Molecule 1" into "Molecule 2."
     # Update transition probability based on properties of "M1," 
     # which was assigned as a "resource" to the transition.
-    ρ4, R1 + M1 --> M2(@t(), rand(4)), preAction => update_prob_transition(state, transition)
+    ρ4,
+    R1 + M1 --> M2(@t(), rand(4)),
+    preAction => update_prob_transition(state, transition)
 end
 
 @prob_init network R1 = 10 R2 = 15
@@ -48,8 +50,8 @@ end
 # We use `@structured` macro, which is a convenience wrapper around `@aagent`),
 # defined in ReactiveDynamics.jl
 @structured network struct M1
-    descriptor
-    time_created
+    descriptor::Any
+    time_created::Any
 end
 
 using Random
@@ -66,7 +68,7 @@ ReactiveDynamics.M1(time, descriptor) = M1("M1" * randstring(4), nothing, descri
     update_prob_transition = function (state, transition)
         if !isnothing(transition) && !isempty(transition.bound_structured_agents)
             bound_agent = first(transition.bound_structured_agents)
-            
+
             transition[:transProbOfSuccess] = min(1.0, sum(bound_agent.descriptor))
         end
     end
@@ -77,8 +79,8 @@ end
 # of ReactiveDynamics.
 @register begin
     @aagent BaseStructuredSpecies AbstractStructuredSpecies struct M2
-        descriptor
-        time_created
+        descriptor::Any
+        time_created::Any
     end
 
     using Random
@@ -92,10 +94,10 @@ ReactiveDynamics.register_structured_species!(network, :M2)
 # Instantiate the network.
 network_instance = ReactionNetworkProblem(network)
 
-for i in 1:2
+for i = 1:2
     add_structured_species!(network_instance, ReactiveDynamics.M1(0.0, rand(4)))
 end
 
 # --------------------------------------------------------------------------------
 # Simulate the network.
-simulate(network_instance, 10)
\ No newline at end of file
+simulate(network_instance, 10)
diff --git a/tutorial/agents-integration/agents_integration.jl b/tutorial/agents-integration/agents_integration.jl
index 25fb6fd..3279902 100644
--- a/tutorial/agents-integration/agents_integration.jl
+++ b/tutorial/agents-integration/agents_integration.jl
@@ -19,8 +19,8 @@ M2_states = "M2_" .* ["A", "B", "C", "D", "E", terminal_states...]
 
 experimental_resources = ["ER1", "ER2", "ER3"]
 
-M1_transition_probs = round.(rand(length(M1_states), length(M1_states)); digits=2)
-M2_transition_probs = round.(rand(length(M2_states), length(M2_states)); digits=2)
+M1_transition_probs = round.(rand(length(M1_states), length(M1_states)); digits = 2)
+M2_transition_probs = round.(rand(length(M2_states), length(M2_states)); digits = 2)
 
 # Ensure forward flow between states and set zero transition prob to terminal states
 for probs in [M1_transition_probs, M2_transition_probs]
@@ -31,8 +31,8 @@ for probs in [M1_transition_probs, M2_transition_probs]
     probs[:, end-1] .= 0
 end
 
-M1_priorities = round.(rand(length(M1_states)); digits=2)
-M2_priorities = 2 * round.(rand(length(M2_states)); digits=2)
+M1_priorities = round.(rand(length(M1_states)); digits = 2)
+M2_priorities = 2 * round.(rand(length(M2_states)); digits = 2)
 
 M1_resources = [rand(1:5, length(experimental_resources)) for _ in M1_states]
 M2_resources = [2 * rand(1:5, length(experimental_resources)) for _ in M2_states]
@@ -43,8 +43,8 @@ M2_durations = rand(2:5, length(M2_states))
 # --------------------------------------------------------------------------------
 # Add initial quantities
 
-M1_initial = [rand(1:10, length(M1_states)-2)..., 0, 0]
-M2_initial = [rand(1:10, length(M2_states)-2)..., 0, 0]
+M1_initial = [rand(1:10, length(M1_states) - 2)..., 0, 0]
+M2_initial = [rand(1:10, length(M2_states) - 2)..., 0, 0]
 
 experimental_resources_initial = rand(1e2:3e2, size(experimental_resources))
 
@@ -76,10 +76,21 @@ end
 # Add transitions for entity class M1.
 
 for (i, state) in enumerate(M1_states[begin:end-2])
-    t = Dict{String, Any}("priority" => M1_priorities[i], "duration" => M1_durations[i], "rate" => state, "name" => "M1")
+    t = Dict{String,Any}(
+        "priority" => M1_priorities[i],
+        "duration" => M1_durations[i],
+        "rate" => state,
+        "name" => "M1",
+    )
 
     # from (left-hand side)
-    push!(t, "from" => [Dict("name" => res, "q" => q) for (res, q) in zip(experimental_resources, M1_resources[i])])
+    push!(
+        t,
+        "from" => [
+            Dict("name" => res, "q" => q) for
+            (res, q) in zip(experimental_resources, M1_resources[i])
+        ],
+    )
     push!(t["from"], Dict("name" => state, "q" => 1))
 
     # to (right-hand side)
@@ -88,7 +99,13 @@ for (i, state) in enumerate(M1_states[begin:end-2])
 
     for (j, state2) in enumerate(M1_states[i:end])
         if M1_transition_probs[j+i-1, i] > 0
-            push!(to, Dict("probability" => M1_transition_probs[j+i-1, i], "reactants" => [Dict("name" => state2, "q" => 1)]))
+            push!(
+                to,
+                Dict(
+                    "probability" => M1_transition_probs[j+i-1, i],
+                    "reactants" => [Dict("name" => state2, "q" => 1)],
+                ),
+            )
         end
     end
 
@@ -98,10 +115,21 @@ end
 # Add transitions for entity class M2.
 
 for (i, state) in enumerate(M2_states[begin:end-2])
-    t = Dict{String, Any}("priority" => M2_priorities[i], "duration" => M2_durations[i], "rate" => state, "name" => "M2")
+    t = Dict{String,Any}(
+        "priority" => M2_priorities[i],
+        "duration" => M2_durations[i],
+        "rate" => state,
+        "name" => "M2",
+    )
 
     # from (left-hand side)
-    push!(t, "from" => [Dict("name" => res, "q" => q) for (res, q) in zip(experimental_resources, M2_resources[i])])
+    push!(
+        t,
+        "from" => [
+            Dict("name" => res, "q" => q) for
+            (res, q) in zip(experimental_resources, M2_resources[i])
+        ],
+    )
     push!(t["from"], Dict("name" => state, "q" => 1))
 
     # to (right-hand side)
@@ -110,7 +138,13 @@ for (i, state) in enumerate(M2_states[begin:end-2])
 
     for (j, state2) in enumerate(M2_states[i:end])
         if M2_transition_probs[j+i-1, i] > 0
-            push!(to, Dict("probability" => M2_transition_probs[j+i-1, i], "reactants" => [Dict("name" => state2, "q" => 1)]))
+            push!(
+                to,
+                Dict(
+                    "probability" => M2_transition_probs[j+i-1, i],
+                    "reactants" => [Dict("name" => state2, "q" => 1)],
+                ),
+            )
         end
     end
 
@@ -137,7 +171,11 @@ function get_subline(d::Vector)
     if isempty(d)
         return "∅"
     elseif haskey(first(d), "probability")
-        sublines = ["($(sd["probability"]), " * join(["$(s["q"]) * $(s["name"])" for s in sd["reactants"]], " + ") * ")" for sd in d]
+        sublines = [
+            "($(sd["probability"]), " *
+            join(["$(s["q"]) * $(s["name"])" for s in sd["reactants"]], " + ") *
+            ")" for sd in d
+        ]
         return "@choose(" * join(sublines, ", ") * ")"
     else
         return join(["$(s["q"]) * $(s["name"])" for s in d], " + ")
@@ -200,7 +238,13 @@ using AlgebraicAgents
     initial_time::Float64
 end
 
-function Controller(name::String, M1_λ::Float64, M2_λ::Float64, time::T, time_step::T) where {T<:Real}
+function Controller(
+    name::String,
+    M1_λ::Float64,
+    M2_λ::Float64,
+    time::T,
+    time_step::T,
+) where {T<:Real}
     return Controller(name, M1_λ, M2_λ, String[], time, time_step, time)
 end
 
@@ -214,26 +258,32 @@ function AlgebraicAgents._step!(c::Controller)
     M1_transitions = filter(x -> x[:transName] === :M1, transitions)
     M2_transitions = filter(x -> x[:transName] === :M2, transitions)
 
-    M1_transitions_delete = isempty(M1_transitions) ? [] : unique(rand(M1_transitions, n_removed_M1))
+    M1_transitions_delete =
+        isempty(M1_transitions) ? [] : unique(rand(M1_transitions, n_removed_M1))
     for trans in M1_transitions_delete
         trans.state = trans[:transCycleTime]
         trans.trans[:transProbOfSuccess] = 0
     end
 
-    M2_transitions_delete = isempty(M2_transitions) ? [] : unique(rand(M2_transitions, n_removed_M2))
+    M2_transitions_delete =
+        isempty(M2_transitions) ? [] : unique(rand(M2_transitions, n_removed_M2))
     for trans in M2_transitions_delete
         trans.state = trans[:transCycleTime]
         trans.trans[:transProbOfSuccess] = 0
     end
 
-    push!(c.log, "t = $(c.current_time) removed compounds: " * join(getname.(union(M1_transitions_delete, M2_transitions_delete)), ", "))
+    push!(
+        c.log,
+        "t = $(c.current_time) removed compounds: " *
+        join(getname.(union(M1_transitions_delete, M2_transitions_delete)), ", "),
+    )
 
     return c.current_time += c.time_step
 end
 
 AlgebraicAgents._projected_to(c::Controller) = c.current_time
 
-c = Controller("controller", 1e-1, 2e-1, 0., 1.);
+c = Controller("controller", 1e-1, 2e-1, 0.0, 1.0);
 
 compound_problem = ⊕(problem, c; name = "compound problem");
 
@@ -253,24 +303,35 @@ draw(compound_problem.inners["network"])
 eval(expr_network_def)
 
 n_primary_resources = 5
-primary_resources = ["R$i" for i in 1:n_primary_resources]
+primary_resources = ["R$i" for i = 1:n_primary_resources]
 
 str_resource_making_transitions = [
-    "p_primary_$i, ∅ --> $res, name => primary_resource_maker_$i" for (i, res) in enumerate(primary_resources)
-] 
+    "p_primary_$i, ∅ --> $res, name => primary_resource_maker_$i" for
+    (i, res) in enumerate(primary_resources)
+]
 
 for (i, res) in enumerate(experimental_resources)
     stoich = rand(1:5, n_primary_resources)
     rate = "p_$i * (" * join(primary_resources, " + ") * ")"
-    transition_lhs = join(["$(stoich[i])" * "$primary_res" for (i, primary_res) in enumerate(primary_resources)], " + ")
-    
-    push!(str_resource_making_transitions, "$rate, $transition_lhs --> $res, name => resource_maker_$i")
+    transition_lhs = join(
+        [
+            "$(stoich[i])" * "$primary_res" for
+            (i, primary_res) in enumerate(primary_resources)
+        ],
+        " + ",
+    )
+
+    push!(
+        str_resource_making_transitions,
+        "$rate, $transition_lhs --> $res, name => resource_maker_$i",
+    )
 end
 
-str_resource_making_params = 
+str_resource_making_params =
     "@prob_params resource_making_network " *
-    join(["p_$i = $(rand(1:3))" for i in 1:length(experimental_resources)], " ") * " " *
-    join(["p_primary_$i = $(rand(1:5))" for i in 1:length(primary_resources)], " ")
+    join(["p_$i = $(rand(1:3))" for i = 1:length(experimental_resources)], " ") *
+    " " *
+    join(["p_primary_$i = $(rand(1:5))" for i = 1:length(primary_resources)], " ")
 
 str_network_def = """
     begin
@@ -286,10 +347,13 @@ expr_network_resource_making_def = striplines(Meta.parseall(str_network_def))
 
 eval(expr_network_resource_making_def)
 
-extended_network = union_acs!(reaction_network, resource_making_network, "resource_making_network")
+extended_network =
+    union_acs!(reaction_network, resource_making_network, "resource_making_network")
 
 #equalize!(extended_network, [Meta.parseall("$res=resource_making_network.$res") for res in experimental_resources])
-str_equalize = "@equalize extended_network " * join(["$res=resource_making_network.$res" for res in experimental_resources], " ")
+str_equalize =
+    "@equalize extended_network " *
+    join(["$res=resource_making_network.$res" for res in experimental_resources], " ")
 
 equalize_expr = striplines(Meta.parseall(str_equalize))
 eval(equalize_expr)
@@ -299,4 +363,4 @@ eval(equalize_expr)
 # Convert network into an AlgAgents hierarchy.
 extended_problem = ReactionNetworkProblem(extended_network; name = "extended_network")
 
-simulate(extended_problem, 100);
\ No newline at end of file
+simulate(extended_problem, 100);

From 4791a81b95997ee88a8cfc9142103d065f775e6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= 
Date: Tue, 12 Mar 2024 14:45:31 +0100
Subject: [PATCH 10/11] Structured species

- An agent now implements a property pointing at
the species.
- Using `@structured` macro, it is possible to set
call a custom agent constructor and assign the species.
- Using `@move` macro, it is possible to
reuse LHS agents on the RHS and modify their species.
---
 src/compilers.jl                      |  14 ++-
 src/interface/agents.jl               |  48 +++++---
 src/interface/reaction_parser.jl      |   5 +-
 src/solvers.jl                        | 170 +++++++++++++++++++-------
 src/state.jl                          |   3 +-
 tutorial/agents-integration/agents.jl |  29 +++--
 6 files changed, 195 insertions(+), 74 deletions(-)

diff --git a/src/compilers.jl b/src/compilers.jl
index b4faba8..eb651ba 100644
--- a/src/compilers.jl
+++ b/src/compilers.jl
@@ -66,7 +66,6 @@ end
 
 reserved_names =
     [:t, :state, :obs, :resample, :solverarg, :take, :log, :periodic, :set_params]
-push!(reserved_names, :state)
 
 function escape_ref(ex, species)
     return if ex isa Symbol
@@ -103,6 +102,15 @@ function wrap_expr(fex, species_names, prm_names, varmap)
         end
     end
 
+    fex = prewalk(fex) do x
+        # here we convert the query metalanguage: @t() -> time(state) etc. 
+        if isexpr(x, :macrocall) && (macroname(x) == :transition)
+            :transition
+        else
+            x
+        end
+    end
+
     # substitute the species names with "pointers" into the state space: S -> state.u[1]
     fex = recursively_substitute_vars!(varmap, fex)
     # substitute the params names with "pointers" into the parameter space: β -> state.p[:β]
@@ -139,8 +147,8 @@ function skip_compile(attr)
            (string(attr) == "trans")
 end
 
-function compile_attrs(acs::ReactionNetworkSchema, structured_species)
-    species_names = setdiff(collect(acs[:, :specName]), structured_species)
+function compile_attrs(acs::ReactionNetworkSchema, structured_token)
+    species_names = setdiff(collect(acs[:, :specName]), structured_token)
 
     prm_names = collect(acs[:, :prmName])
     varmap = Dict([name => :(state.u[$i]) for (i, name) in enumerate(species_names)])
diff --git a/src/interface/agents.jl b/src/interface/agents.jl
index 842a7f8..fcd5558 100644
--- a/src/interface/agents.jl
+++ b/src/interface/agents.jl
@@ -1,15 +1,17 @@
-export AbstractStructuredSpecies, BaseStructuredSpecies
-export @structured
-export add_structured_species!
+export AbstractStructuredToken, BaseStructuredToken
+export @structured_token
+export add_structured_token!
 
 # Abstract supertype of all structured species.
-abstract type AbstractStructuredSpecies <: AbstractAlgebraicAgent end
+abstract type AbstractStructuredToken <: AbstractAlgebraicAgent end
 
 # It comes handy to keep track of the transition the entity is assigned to (if).
 # In general, we will probably assume that each "structured agent" type implements this field.
 # Otherwise, it would be possible to implement getter and setter interface and use it from within ReaDyn.
-@aagent FreeAgent struct BaseStructuredSpecies
+@aagent FreeAgent struct BaseStructuredToken
+    species::Union{Nothing,Symbol}
     bound_transition::Union{Nothing,ReactiveDynamics.Transition}
+    past_bonds::Vector{Tuple{Symbol,Float64,Transition}}
 end
 
 # We use this to let the network know that the type is structured.
@@ -25,33 +27,45 @@ function register_structured_species!(reaction_network, type)
 end
 
 # Convenience macro to define structured species.
-macro structured(network, type)
-    name = Docs.namify(type.args[2])
-
+macro structured_token(network, type)
     quote
         $(AlgebraicAgents.aagent(
-            BaseStructuredSpecies,
-            AbstractStructuredSpecies,
+            BaseStructuredToken,
+            AbstractStructuredToken,
             type,
             ReactiveDynamics,
         ))
-        register_structured_species!($(esc(network)), $(QuoteNode(name)))
     end
 end
 
 # Add a structured agent instance to an instance of a reaction network.
-function add_structured_species!(problem::ReactionNetworkProblem, agent)
-    return entangle!(getagent(problem, "structured/$(nameof(typeof(agent)))"), agent)
+function add_structured_token!(problem::ReactionNetworkProblem, agent)
+    return entangle!(getagent(problem, "structured"), agent)
 end
 
 import AlgebraicAgents
 
 # By default, structured agents have no evolutionary rule.
-AlgebraicAgents._projected_to(::AbstractStructuredSpecies) = nothing
-AlgebraicAgents._step!(::AbstractStructuredSpecies) = nothing
+AlgebraicAgents._projected_to(::AbstractStructuredToken) = nothing
+AlgebraicAgents._step!(::AbstractStructuredToken) = nothing
 
 # Tell if an agent is assigned to a transition, as a resource.
-isblocked(a) = !isnothing(a.bound_transition)
+isblocked(a::AbstractStructuredToken) = !isnothing(get_bound_transition(a))
+
+# Add a record that an agent was used as "species" in a "transition".
+function add_to_log!(a::AbstractStructuredToken, species::Symbol, t, transition::Transition)
+    return push!(a.past_bonds, (species, Float64(t), transition))
+end
+
+# Set the transition a token is bound to.
+get_bound_transition(a::AbstractStructuredToken) = a.bound_transition
+function set_bound_transition!(a::AbstractStructuredToken, t::Union{Nothing,Transition})
+    return a.bound_transition = t
+end
 
 # Priority with which an unbound agent will be assigned to a transition.
-priority(a, transition) = 0.0
+priority(a::AbstractStructuredToken, transition) = 0.0
+
+# What species (place) is an agent currently assigned to.
+get_species(a::AbstractStructuredToken) = a.species
+set_species!(a::AbstractStructuredToken, species::Symbol) = a.species = species
diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl
index 8ad0651..1f83113 100644
--- a/src/interface/reaction_parser.jl
+++ b/src/interface/reaction_parser.jl
@@ -63,6 +63,9 @@ function recursive_find_reactants!(
         for i = 2:length(ex.args)
             recursive_find_reactants!(ex.args[i], mult, mods, reactants)
         end
+    elseif isexpr(ex, :call) ||
+           (ex.head == :macrocall && macroname(ex) ∈ [:structured, :move])
+        push!(reactants, FoldedReactant(ex, mult, mods))
     elseif ex.head == :macrocall
         mods = copy(mods)
         macroname(ex) in species_modalities && push!(mods, macroname(ex))
@@ -71,8 +74,6 @@ function recursive_find_reactants!(
             4:length(ex.args),
         )
         recursive_find_reactants!(ex.args[3], mult, mods, reactants)
-    elseif isexpr(ex, :call)
-        push!(reactants, FoldedReactant(ex, mult, mods))
     else
         @error("malformed reaction")
     end
diff --git a/src/solvers.jl b/src/solvers.jl
index cb86797..9bf982c 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -35,7 +35,7 @@ function get_reqs_ongoing!(reqs, qs, state)
             in(:rate, tok.modality) &&
                 (state.ongoing_transitions[i][:transCycleTime] > 0) &&
                 (reqs[tok.index, i] += qs[i] * tok.stoich * state.dt)
-            if in(:rate, tok.modality) && in(tok.species, state.structured_species)
+            if in(:rate, tok.modality) && in(tok.species, state.structured_token)
                 error(
                     "Modality `:rate` is not supported for structured species in transition $(trans[:transName]).",
                 )
@@ -170,6 +170,8 @@ function evolve!(state)
     state.u .-= sum(allocs; dims = 2)
     actual_allocs .+= sum(allocs; dims = 2)
 
+    structured_token = collect(values(inners(getagent(state, "structured"))))
+
     # add spawned transitions to the heap
     for i in parts(state, :T)
         if qs[i] != 0
@@ -179,6 +181,7 @@ function evolve!(state)
                 get_sampled_transition(state, i),
                 AbstractAlgebraicAgent[],
                 AbstractAlgebraicAgent[],
+                [],
                 state.t,
                 qs[i],
                 0.0,
@@ -186,29 +189,35 @@ function evolve!(state)
             push!(state.ongoing_transitions, transition)
 
             bound = transition.bound_structured_agents
+            structured_to_agents = transition.structured_to_agents
 
             for (j, type) in enumerate(state.acs[:, :specName])
-                if type ∈ state.structured_species
+                if type ∈ state.structured_token
                     if !isinteger(allocs[j, i])
                         error(
                             "For structured species, stoichiometry coefficient must be integer in transition $i.",
                         )
                     end
 
-                    all_of_type = collect(
-                        values(inners(getagent(state, "structured/$(string(type))"))),
+                    available_species = filter(
+                        a -> get_species(a) == type && !isblocked(a),
+                        structured_token,
                     )
-                    filter!(!isblocked, all_of_type)
+
                     sort!(
-                        all_of_type;
+                        available_species;
                         by = a -> priority(a, state.acs[i, :transName]),
                         rev = true,
                     )
 
                     ix = 1
-                    while allocs[j, i] > 0 && ix <= length(all_of_type)
-                        all_of_type[ix].bound_transition = transition
-                        push!(bound, all_of_type[ix])
+                    while allocs[j, i] > 0 && ix <= length(available_species)
+                        set_bound_transition!(available_species[ix], transition)
+
+                        push!(bound, available_species[ix])
+                        push!(structured_to_agents, type => available_species[ix])
+                        add_to_log!(available_species[ix], type, state.t, transition)
+
                         allocs[j, i] -= 1
                         ix += 1
                     end
@@ -250,30 +259,40 @@ function evolve!(state)
         transition = state.ongoing_transitions[i]
         if qs[i] != 0
             transition.state += qs[i] * state.dt
+
             bound = transition.nonblock_structured_agents
+            structured_to_agents = transition.structured_to_agents
 
             for (j, type) in enumerate(state.acs[:, :specName])
-                if type ∈ state.structured_species
+                if type ∈ state.structured_token
                     if !isinteger(allocs[j, i])
                         error(
                             "For structured species, stoichiometry coefficient must be integer in transition $i.",
                         )
                     end
 
-                    all_of_type = collect(
-                        values(inners(getagent(state, "structured/$(string(type))"))),
+                    available_species = filter(
+                        a -> get_species(a) == type && !isblocked(a),
+                        structured_token,
                     )
-                    filter!(!isblocked, all_of_type)
+
                     sort!(
-                        all_of_type;
+                        available_species;
                         by = a -> priority(a, state.acs[i, :transName]),
                         rev = true,
                     )
 
                     ix = 1
-                    while allocs[j, i] > 0 && ix <= length(all_of_type)
-                        all_of_type[ix].bound_transition = transition
-                        push!(bound, all_of_type[ix])
+                    while allocs[j, i] > 0 && ix <= length(available_species)
+                        set_bound_transition!(
+                            available_species[ix].bound_transition,
+                            transition,
+                        )
+
+                        push!(bound, available_species[ix])
+                        push!(structured_to_agents, type => available_species[ix])
+                        add_to_log!(available_species[ix], type, state.t, transition)
+
                         allocs[j, i] -= 1
                         ix += 1
                     end
@@ -306,6 +325,64 @@ function event_action!(state)
     end
 end
 
+function allocate_for_move(t::Transition, s::Symbol)
+    return t.bound_structured_agents ∩
+           map(x -> x[2], filter(x -> x[1] == s, t.structured_to_agents))
+end
+
+function structured_rhs(expr::Expr, state, transition)
+    if isexpr(expr, :macrocall) && macroname(expr) == :structured
+        expr = quote
+            token = $(expr.args[end-1])
+            species = $(expr.args[end])
+
+            return token, species
+        end
+
+        token, species = context_eval(state, transition, state.wrap_fun(expr))
+        set_species!(token, Symbol(species))
+
+        entangle!(getagent(state, "structured"), token)
+
+        return token, get_species(token)
+    elseif isexpr(expr, :macrocall) && macroname(expr) == :move
+        expr = quote
+            species_from = $(expr.args[end-1])
+            species_to = $(expr.args[end])
+
+            return species_from, species_to
+        end
+
+        species_from, species_to =
+            Symbol.(context_eval(state, transition, state.wrap_fun(expr)))
+
+        tokens =
+            filter(x -> get_species(x) == species_from, transition.bound_structured_agents)
+        if !isempty(tokens)
+            token = first(tokens)
+            entangle!(getagent(state, "structured"), token)
+
+            set_species!(token, species_to)
+            ix = findfirst(
+                i -> transition.bound_structured_agents[i] == token,
+                eachindex(transition.bound_structured_agents),
+            )
+            deleteat!(transition.bound_structured_agents, ix)
+            set_bound_transition!(token, nothing)
+
+            return token, species_to
+        else
+            @error "Not enough tokens to allocate for a move."
+        end
+
+    else
+        token = context_eval(state, transition, state.wrap_fun(expr))
+        entangle!(getagent(state, "structured"), token)
+
+        return token, get_species(token)
+    end
+end
+
 # collect terminated transitions
 function finish!(state)
     val_reward = 0
@@ -327,19 +404,13 @@ function finish!(state)
 
         for r in extract_reactants(trans_[:transRHS], state)
             if r.species isa Expr
-                if !(r.species.args[1] ∈ state.structured_species)
-                    error("Unresolved structured species species $(r.species.args[1]).")
-                end
-
-                i = find_index(r.species.args[1], state)
                 stoich = context_eval(state, trans_, state.wrap_fun(r.stoich))
 
-                state.u[i] += q * stoich
-                val_reward += state[i, :specReward] * q * stoich
-
-                for _ = 1:q
-                    a = context_eval(state, trans_, state.wrap_fun(r.species))
-                    entangle!(getagent(state, "structured/$(r.species.args[1])"), a)
+                for _ = 1:(q*stoich)
+                    token, species = structured_rhs(r.species, state, trans_)
+                    i = find_index(species, state)
+                    state.u[i] += 1
+                    val_reward += state[i, :specReward]
                 end
             else
                 i = find_index(r.species, state)
@@ -356,9 +427,13 @@ function finish!(state)
                     trans_.q *
                     tok.stoich *
                     (in(:rate, tok.modality) ? trans_[:transCycleTime] : 1)
-                if tok.species ∈ state.structured_species
+                if tok.species ∈ state.structured_token
                     for _ = 1:(trans_.q*tok.stoich)
-                        trans_.bound_structured_agents[begin].bound_transition = nothing
+                        isempty(trans_.bound_structured_agents) && break
+                        set_bound_transition!(
+                            trans_.bound_structured_agents[begin].bound_transition,
+                            nothing,
+                        )
                         deleteat!(trans_.bound_structured_agents, 1)
                     end
                 end
@@ -372,9 +447,12 @@ function finish!(state)
                 end
 
                 state.u[tok.index] += trans_.q * tok.stoich
-                if tok.species ∈ state.structured_species
+                if tok.species ∈ state.structured_token
                     for _ = 1:(trans_.q*tok.stoich)
-                        trans_.nonblock_structured_agents[begin].bound_transition = nothing
+                        set_bound_transition!(
+                            trans_.nonblock_structured_agents[begin].bound_transition,
+                            nothing,
+                        )
                         deleteat!(trans_.nonblock_structured_agents, 1)
                     end
                 end
@@ -439,6 +517,7 @@ function ReactionNetworkProblem(
         acs[i, :metaKeyword] => acs[i, :metaVal] for i in parts(acs, :M) if
         !isnothing(acs[i, :metaKeyword]) && !isnothing(acs[i, :metaVal])
     ])
+
     merge!(keywords, Dict(collect(kwargs)))
     merge!(keywords, Dict(:strategy => get(keywords, :alloc_strategy, :weighted)))
 
@@ -446,10 +525,10 @@ function ReactionNetworkProblem(
 
     acs = remove_choose(acs)
 
-    structured_species_names =
+    structured_token_names =
         acs[filter(i -> acs[i, :specStructured], 1:nparts(acs, :S)), :specName]
 
-    attrs, transitions, wrap_fun = compile_attrs(acs, structured_species_names)
+    attrs, transitions, wrap_fun = compile_attrs(acs, structured_token_names)
     transition_recipes = transitions
     u0_init = zeros(nparts(acs, :S))
 
@@ -482,6 +561,7 @@ function ReactionNetworkProblem(
         "t" => Float64[],
         (string(name) => Float64[] for name in acs[:, :specName])...,
     )
+
     network = ReactionNetworkProblem(
         name,
         acs,
@@ -490,7 +570,7 @@ function ReactionNetworkProblem(
         u0_init,
         merge(p, Dict(:strategy => get(keywords, :alloc_strategy, :weighted))),
         keywords[:tspan][1],
-        Symbol[],
+        structured_token_names,
         keywords[:tspan],
         get(keywords, :tstep, 1),
         transitions,
@@ -503,12 +583,6 @@ function ReactionNetworkProblem(
 
     entangle!(network, FreeAgent("structured"))
 
-    structured_species = filter(i -> acs[i, :specStructured], 1:nparts(acs, :S))
-    for i in structured_species
-        push!(network.structured_species, acs[i, :specName])
-        entangle!(getagent(network, "structured"), FreeAgent(string(acs[i, :specName])))
-    end
-
     save!(network)
 
     return network
@@ -526,18 +600,26 @@ function AlgebraicAgents._reinit!(state::ReactionNetworkProblem)
 end
 
 function update_u_structured!(state)
-    for (i, type) in enumerate(state.acs[:, :specName])
-        structured_agents_type = values(inners(getagent(state, "structured/$type")))
-        state.u[i] = count(!isblocked, structured_agents_type)
+    structured_tokens = collect(values(inners(getagent(state, "structured"))))
+    for (i, species) in enumerate(state.acs[:, :specName])
+        if state.acs[i, :specStructured]
+            state.u[i] =
+                count(a -> get_species(a) == species && !isblocked(a), structured_tokens)
+        end
     end
+
+    return state.u
 end
 
 function AlgebraicAgents._step!(state::ReactionNetworkProblem)
     free_blocked_species!(state)
+    update_u_structured!(state)
     update_observables(state)
     sample_transitions!(state)
     evolve!(state)
+    update_u_structured!(state)
     finish!(state)
+    update_u_structured!(state)
 
     event_action!(state)
 
diff --git a/src/state.jl b/src/state.jl
index ed7a7f9..d0eef41 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -18,6 +18,7 @@ Ongoing transition auxiliary structure.
 
     bound_structured_agents::Vector{AbstractAlgebraicAgent}
     nonblock_structured_agents::Vector{AbstractAlgebraicAgent}
+    structured_to_agents::Vector
 
     t::Float64
     q::Float64
@@ -46,7 +47,7 @@ end
     p::Any
     t::Float64
 
-    structured_species::Vector{Symbol}
+    structured_token::Vector{Symbol}
 
     tspan::Tuple{Float64,Float64}
     dt::Float64
diff --git a/tutorial/agents-integration/agents.jl b/tutorial/agents-integration/agents.jl
index 5300f0d..b021720 100644
--- a/tutorial/agents-integration/agents.jl
+++ b/tutorial/agents-integration/agents.jl
@@ -35,6 +35,13 @@ network = @ReactionNetworkSchema
     ρ4,
     R1 + M1 --> M2(@t(), rand(4)),
     preAction => update_prob_transition(state, transition)
+
+    # R2 + M1 --> M2(@t(), rand(4))
+    5.0, R2 + M1 --> @structured(M2(@t(), rand(4)), :A)
+    1.0, R2 + A --> @structured(M2(@t(), rand(4)), f_species(@transition))
+
+    1.0, R2 + M1 --> @move(M1, :M2)
+    1.0, R2 + M1 --> @move(M1, :C)
 end
 
 @prob_init network R1 = 10 R2 = 15
@@ -49,7 +56,7 @@ end
 
 # We use `@structured` macro, which is a convenience wrapper around `@aagent`),
 # defined in ReactiveDynamics.jl
-@structured network struct M1
+@structured_token network struct M1
     descriptor::Any
     time_created::Any
 end
@@ -60,7 +67,9 @@ using Random
 # Accordingly, we have to explicitly declare the scope.
 using ReactiveDynamics: M1
 
-ReactiveDynamics.M1(time, descriptor) = M1("M1" * randstring(4), nothing, descriptor, time)
+function ReactiveDynamics.M1(time, descriptor)
+    return M1("M1" * randstring(4), :M1, nothing, [], descriptor, time)
+end
 
 # We define the function which updates the transition probability.
 # This has to be accessible from within the name scope of ReactiveDynamics.
@@ -69,35 +78,41 @@ ReactiveDynamics.M1(time, descriptor) = M1("M1" * randstring(4), nothing, descri
         if !isnothing(transition) && !isempty(transition.bound_structured_agents)
             bound_agent = first(transition.bound_structured_agents)
 
-            transition[:transProbOfSuccess] = min(1.0, sum(bound_agent.descriptor))
+            transition[:transProbOfSuccess] = 1.0#min(1.0, sum(bound_agent.descriptor))
         end
     end
+
+    f_species(transition) = :B
 end
 
 # Alternatively, we can define a structured agent type using
 # the usual `@aagent` macro. This must be evaluated inside the scope
 # of ReactiveDynamics.
 @register begin
-    @aagent BaseStructuredSpecies AbstractStructuredSpecies struct M2
+    @aagent BaseStructuredToken AbstractStructuredToken struct M2
         descriptor::Any
         time_created::Any
     end
 
     using Random
-    M2(time, descriptor) = M2("M2" * randstring(4), nothing, descriptor, time)
+    M2(time, descriptor) = M2("M2" * randstring(4), :M2, nothing, [], descriptor, time)
 end
 
 # Let the network know that the species is structured.
-ReactiveDynamics.register_structured_species!(network, :M2)
+for species in [:M1, :M2, :A, :B, :C]
+    ReactiveDynamics.register_structured_species!(network, species)
+end
 
 # --------------------------------------------------------------------------------
 # Instantiate the network.
 network_instance = ReactionNetworkProblem(network)
 
 for i = 1:2
-    add_structured_species!(network_instance, ReactiveDynamics.M1(0.0, rand(4)))
+    add_structured_token!(network_instance, ReactiveDynamics.M1(0.0, rand(4)))
 end
 
 # --------------------------------------------------------------------------------
 # Simulate the network.
 simulate(network_instance, 10)
+
+# tokens = collect(values(inners(getagent(network_instance, "structured"))))

From c99d4ce189c17a29cb2407172897d1c69333bc7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?B=C3=ADma=2C=20Jan?= 
Date: Tue, 23 Apr 2024 15:32:43 +0200
Subject: [PATCH 11/11] Add tutorial w/ CEED integration

---
 docs/build/index.html                         |   2 +-
 docs/build/search_index.js                    |   2 +-
 docs/src/index.md                             |   2 +-
 src/ReactiveDynamics.jl                       |   2 +-
 src/compilers.jl                              |  17 +-
 src/interface/create.jl                       |   9 +-
 src/solvers.jl                                |  44 +++-
 tutorial/agents-integration/Project.toml      |   7 +
 tutorial/agents-integration/agents.jl         |  22 +-
 .../agents-integration/ceed_integration.jl    | 236 ++++++++++++++++++
 .../agents-integration/experimental_setup.jl  |  56 +++++
 tutorial/toy_pharma_model.jl                  |   6 +-
 12 files changed, 361 insertions(+), 44 deletions(-)
 create mode 100644 tutorial/agents-integration/ceed_integration.jl
 create mode 100644 tutorial/agents-integration/experimental_setup.jl

diff --git a/docs/build/index.html b/docs/build/index.html
index 991dc9f..b340372 100644
--- a/docs/build/index.html
+++ b/docs/build/index.html
@@ -8,7 +8,7 @@
 @push acs 1.0 X ⟶ Y 
 @prob_init acs X=1 Y=2 XY=α
 @prob_params acs γ=1 α=4
-@solve_and_plot acs

Modify a model

We list common transition attributes:

attributeinterpretation
transPrioritypriority of a transition (influences resource allocation)
transProbOfSuccessprobability that a transition terminates successfully
transCapacitymaximum number of concurrent instances of the transition
transCycleTimeduration of a transition's instance (adjusted by resource allocation)
transMaxLifeTimemaximal duration of a transition's instance
transPostActionaction to be executed once a transition's instance terminates
transNamename of a transition

We list common species attributes:

attributeinterpretation
specInitUncertaintyuncertainty about variable's initial state (modelled as Gaussian standard deviation)
specInitValinitial value of a variable

Moreover, it is possible to specify the semantics of the "rate" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....

For deterministic "rates", use @per_step(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover

ReactiveDynamics.@akaMacro

Alias object name in an acs.

Default names

nameshort name
speciesS
transitionT
actionA
eventE
paramP
metaM

Examples

@aka acs species=resource transition=reaction
ReactiveDynamics.@modeMacro

Set species modality.

Supported modalities

  • nonblock
  • conserved
  • rate

Examples

@mode acs (r"proj\w+", r"experimental\w+") conserved
+@solve_and_plot acs

Modify a model

We list common transition attributes:

attributeinterpretation
transPrioritypriority of a transition (influences resource allocation)
transProbOfSuccessprobability that a transition terminates successfully
transCapacitymaximum number of concurrent instances of the transition
transCycleTimeduration of a transition's instance (adjusted by resource allocation)
transMaxLifeTimemaximal duration of a transition's instance
transPostActionaction to be executed once a transition's instance terminates
transNamename of a transition

We list common species attributes:

attributeinterpretation
specInitUncertaintyuncertainty about variable's initial state (modelled as Gaussian standard deviation)
specInitValinitial value of a variable

Moreover, it is possible to specify the semantics of the "rate" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....

For deterministic "rates", use @deterministic(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover

ReactiveDynamics.@akaMacro

Alias object name in an acs.

Default names

nameshort name
speciesS
transitionT
actionA
eventE
paramP
metaM

Examples

@aka acs species=resource transition=reaction
ReactiveDynamics.@modeMacro

Set species modality.

Supported modalities

  • nonblock
  • conserved
  • rate

Examples

@mode acs (r"proj\w+", r"experimental\w+") conserved
 @mode acs (S, I) conserved
 @mode acs S conserved
ReactiveDynamics.@name_transitionMacro

Set name of a transition in the model.

Examples

@name_transition acs 1="name"
 @name_transition acs name="transition_name"
diff --git a/docs/build/search_index.js b/docs/build/search_index.js
index 6b6953e..7c23342 100644
--- a/docs/build/search_index.js
+++ b/docs/build/search_index.js
@@ -1,3 +1,3 @@
 var documenterSearchIndex = {"docs":
-[{"location":"index.html#API-Documentation","page":"API Documentation","title":"API Documentation","text":"","category":"section"},{"location":"index.html#Create-a-model","page":"API Documentation","title":"Create a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@ReactionNetwork","category":"page"},{"location":"index.html#ReactiveDynamics.@ReactionNetwork","page":"API Documentation","title":"ReactiveDynamics.@ReactionNetwork","text":"Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.\n\nMost arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.\n\nCustom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.\n\nExamples\n\nacs = @ReactionNetworkSchema begin\n    1.0, X ⟶ Y\n    1.0, X ⟶ Y, priority=>6., prob=>.7, capacity=>3.\n    1.0, ∅ --> (Poisson(.3γ)X, Poisson(.5)Y)\n    (XY > 100) && (XY -= 1)\nend\n@push acs 1.0 X ⟶ Y \n@prob_init acs X=1 Y=2 XY=α\n@prob_params acs γ=1 α=4\n@solve_and_plot acs\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Modify-a-model","page":"API Documentation","title":"Modify a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common transition attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\ntransPriority priority of a transition (influences resource allocation)\ntransProbOfSuccess probability that a transition terminates successfully\ntransCapacity maximum number of concurrent instances of the transition\ntransCycleTime duration of a transition's instance (adjusted by resource allocation)\ntransMaxLifeTime maximal duration of a transition's instance\ntransPostAction action to be executed once a transition's instance terminates\ntransName name of a transition","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common species attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\nspecInitUncertainty uncertainty about variable's initial state (modelled as Gaussian standard deviation)\nspecInitVal initial value of a variable","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"Moreover, it is possible to specify the semantics of the \"rate\" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"For deterministic \"rates\", use @per_step(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@add_species\n@aka\n@mode\n@name_transition","category":"page"},{"location":"index.html#ReactiveDynamics.@add_species","page":"API Documentation","title":"ReactiveDynamics.@add_species","text":"Add new species to a model.\n\nExamples\n\n@add_species acs S I R\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@aka","page":"API Documentation","title":"ReactiveDynamics.@aka","text":"Alias object name in an acs.\n\nDefault names\n\nname short name\nspecies S\ntransition T\naction A\nevent E\nparam P\nmeta M\n\nExamples\n\n@aka acs species=resource transition=reaction\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@mode","page":"API Documentation","title":"ReactiveDynamics.@mode","text":"Set species modality.\n\nSupported modalities\n\nnonblock\nconserved\nrate\n\nExamples\n\n@mode acs (r\"proj\\w+\", r\"experimental\\w+\") conserved\n@mode acs (S, I) conserved\n@mode acs S conserved\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@name_transition","page":"API Documentation","title":"ReactiveDynamics.@name_transition","text":"Set name of a transition in the model.\n\nExamples\n\n@name_transition acs 1=\"name\"\n@name_transition acs name=\"transition_name\"\n@name_transition acs \"name\"=\"transition_name\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Resource-costs","page":"API Documentation","title":"Resource costs","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@cost\n@valuation\n@reward","category":"page"},{"location":"index.html#ReactiveDynamics.@cost","page":"API Documentation","title":"ReactiveDynamics.@cost","text":"Set cost.\n\nExamples\n\n@cost model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@valuation","page":"API Documentation","title":"ReactiveDynamics.@valuation","text":"Set valuation.\n\nExamples\n\n@valuation model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@reward","page":"API Documentation","title":"ReactiveDynamics.@reward","text":"Set reward.\n\nExamples\n\n@reward model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Add-reactions","page":"API Documentation","title":"Add reactions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@push\n@jump\n@periodic","category":"page"},{"location":"index.html#ReactiveDynamics.@push","page":"API Documentation","title":"ReactiveDynamics.@push","text":"Add reactions to an acset.\n\nExamples\n\n@push sir_acs β*S*I*tdecay(@time()) S+I --> 2I name=>SI2I\n@push sir_acs begin \n    ν*I, I --> R, name=>I2R\n    γ, R --> S, name=>R2S\nend\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@jump","page":"API Documentation","title":"ReactiveDynamics.@jump","text":"Add a jump process (with specified Poisson intensity per unit time step) to a model.\n\nExamples\n\n@jump acs λ Z += rand(Poisson(1.))\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@periodic","page":"API Documentation","title":"ReactiveDynamics.@periodic","text":"Add a periodic callback to a model.\n\nExamples\n\n@periodic acs 1. X += 1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Set-initial-values,-uncertainty,-and-solver-arguments","page":"API Documentation","title":"Set initial values, uncertainty, and solver arguments","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@prob_init\n@prob_uncertainty\n@prob_params\n@prob_meta","category":"page"},{"location":"index.html#ReactiveDynamics.@prob_init","page":"API Documentation","title":"ReactiveDynamics.@prob_init","text":"Set initial values of species in an acset.\n\nExamples\n\n@prob_init acs X=1 Y=2 Z=h(α)\n@prob_init acs [1., 2., 3.]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_uncertainty","page":"API Documentation","title":"ReactiveDynamics.@prob_uncertainty","text":"Set uncertainty in initial values of species in an acset (stderr).\n\nExamples\n\n@prob_uncertainty acs X=.1 Y=.2\n@prob_uncertainty acs [.1, .2,]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_params","page":"API Documentation","title":"ReactiveDynamics.@prob_params","text":"Set parameter values in an acset.\n\nExamples\n\n@prob_params acs α=1. β=2.\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_meta","page":"API Documentation","title":"ReactiveDynamics.@prob_meta","text":"Set model metadata (e.g. solver arguments)\n\nExamples\n\n@prob_meta acs tspan=(0, 100.) schedule=schedule_weighted!\n@prob_meta sir_acs tspan=250 tstep=1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-unions","page":"API Documentation","title":"Model unions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@join\n@equalize","category":"page"},{"location":"index.html#ReactiveDynamics.@join","page":"API Documentation","title":"ReactiveDynamics.@join","text":"@join models... [equalize...]\n\nPerforms join of models and identifies model variables, as specified.\n\nModel variables / parameter values and metadata are propagated; the last model takes precedence.\n\nExamples\n\n@join acs1 acs2 @catchall(A)=acs2.Z @catchall(XY) @catchall(B)\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@equalize","page":"API Documentation","title":"ReactiveDynamics.@equalize","text":"Identify (collapse) a set of species in a model.\n\nExamples\n\n@join acs acs1.A=acs2.A B=C\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-import-and-export","page":"API Documentation","title":"Model import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_network\n@export_network","category":"page"},{"location":"index.html#ReactiveDynamics.@import_network","page":"API Documentation","title":"ReactiveDynamics.@import_network","text":"Import a model from a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@import_network \"model.toml\"\n@import_network \"csv/model.toml\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_network","page":"API Documentation","title":"ReactiveDynamics.@export_network","text":"Export model to a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@export_network acs \"acs_data.toml\" # as a TOML\n@export_network acs \"csv/model.csv\" # as a CSV\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Solution-import-and-export","page":"API Documentation","title":"Solution import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_solution\n@export_solution_as_table\n@export_solution_as_csv\n@export_solution","category":"page"},{"location":"index.html#ReactiveDynamics.@import_solution","page":"API Documentation","title":"ReactiveDynamics.@import_solution","text":"@import_solution \"sol.jld2\"\n@import_solution \"sol.jld2\" sol\n\nImport a solution from a file.\n\nExamples\n\n@import_solution \"sir_acs_sol/serialized/sol.jld2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_table","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_table","text":"@export_solution_as_table sol\n\nExport a solution as a DataFrame.\n\nExamples\n\n@export_solution_as_table sol\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_csv","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_csv","text":"@export_solution_as_csv sol\n@export_solution_as_csv sol \"sol.csv\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution_as_csv sol \"sol.csv\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution","page":"API Documentation","title":"ReactiveDynamics.@export_solution","text":"@export_solution sol\n@export_solution sol \"sol.jld2\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution sol \"sol.jdl2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Problematize,sSolve,-and-plot","page":"API Documentation","title":"Problematize,sSolve, and plot","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@problematize\n@solve\n@plot","category":"page"},{"location":"index.html#ReactiveDynamics.@problematize","page":"API Documentation","title":"ReactiveDynamics.@problematize","text":"Convert a model to a DiscreteProblem. If passed a problem instance, return the instance.\n\nExamples\n\n@problematize acs tspan=1:100\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@solve","page":"API Documentation","title":"ReactiveDynamics.@solve","text":"Solve the problem. Solverargs passed at the calltime take precedence.\n\nExamples\n\n@solve prob\n@solve prob tspan=1:100\n@solve prob tspan=100 trajectories=20\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@plot","page":"API Documentation","title":"ReactiveDynamics.@plot","text":"Plot the solution (summary).\n\nExamples\n\n@plot sol plot_type=summary\n@plot sol plot_type=allocation # not supported for ensemble solutions!\n@plot sol plot_type=valuations # not supported for ensemble solutions!\n@plot sol plot_type=new_transitions # not supported for ensemble solutions!\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Optimization-and-fitting","page":"API Documentation","title":"Optimization and fitting","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@optimize\n@fit\n@fit_and_plot\n@build_solver","category":"page"},{"location":"index.html#ReactiveDynamics.@optimize","page":"API Documentation","title":"ReactiveDynamics.@optimize","text":"@optimize acset objective ... ... opts...\n\nTake an acset and optimize given functional.\n\nObjective is an expression which may reference the model's variables and parameters, i.e., A+β. The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nBy default, the functional is minimized. Specify objective=max to perform maximization. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\n@optimize acs abs(A-B) A B=20. α=2. lower_bounds=0 upper_bounds=100\n@optimize acss abs(A-B) A B=20. α=2. upper_bounds=[200,300,400] maxeval=200 objective=min\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit","page":"API Documentation","title":"ReactiveDynamics.@fit","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset and fit initial values and parameters to empirical data.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit_and_plot","page":"API Documentation","title":"ReactiveDynamics.@fit_and_plot","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset, fit initial values and parameters to empirical data, and plot the result.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@build_solver","page":"API Documentation","title":"ReactiveDynamics.@build_solver","text":"@build_solver acset ... ... opts...\n\nTake an acset and export a solution as a function of free vars and free parameters.\n\nExamples\n\nsolver = @build_solver acs S α β # function of variable S and parameters α, β\nsolver([S, α, β])\n\n\n\n\n\n","category":"macro"}]
+[{"location":"index.html#API-Documentation","page":"API Documentation","title":"API Documentation","text":"","category":"section"},{"location":"index.html#Create-a-model","page":"API Documentation","title":"Create a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@ReactionNetwork","category":"page"},{"location":"index.html#ReactiveDynamics.@ReactionNetwork","page":"API Documentation","title":"ReactiveDynamics.@ReactionNetwork","text":"Macro that takes an expression corresponding to a reaction network and outputs an instance of TheoryReactionNetwork that can be converted to a DiscreteProblem or solved directly.\n\nMost arrows accepted (both right, left, and bi-drectional arrows). Use 0 or ∅ for annihilation/creation to/from nothing.\n\nCustom functions and sampleable objects can be used as numeric parameters. Note that these have to be accessible from ReactiveDynamics's source code.\n\nExamples\n\nacs = @ReactionNetworkSchema begin\n    1.0, X ⟶ Y\n    1.0, X ⟶ Y, priority=>6., prob=>.7, capacity=>3.\n    1.0, ∅ --> (Poisson(.3γ)X, Poisson(.5)Y)\n    (XY > 100) && (XY -= 1)\nend\n@push acs 1.0 X ⟶ Y \n@prob_init acs X=1 Y=2 XY=α\n@prob_params acs γ=1 α=4\n@solve_and_plot acs\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Modify-a-model","page":"API Documentation","title":"Modify a model","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common transition attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\ntransPriority priority of a transition (influences resource allocation)\ntransProbOfSuccess probability that a transition terminates successfully\ntransCapacity maximum number of concurrent instances of the transition\ntransCycleTime duration of a transition's instance (adjusted by resource allocation)\ntransMaxLifeTime maximal duration of a transition's instance\ntransPostAction action to be executed once a transition's instance terminates\ntransName name of a transition","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"We list common species attributes:","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"attribute interpretation\nspecInitUncertainty uncertainty about variable's initial state (modelled as Gaussian standard deviation)\nspecInitVal initial value of a variable","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"Moreover, it is possible to specify the semantics of the \"rate\" term. By default, at each time step n ~ Poisson(rate * dt) instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use @ct(cycle_time), e.g., @ct(ex), A --> B, .... This is a shorthand for 1/ex, A --> B, ....","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"For deterministic \"rates\", use @deterministic(ex). Here, ex evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover","category":"page"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@add_species\n@aka\n@mode\n@name_transition","category":"page"},{"location":"index.html#ReactiveDynamics.@add_species","page":"API Documentation","title":"ReactiveDynamics.@add_species","text":"Add new species to a model.\n\nExamples\n\n@add_species acs S I R\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@aka","page":"API Documentation","title":"ReactiveDynamics.@aka","text":"Alias object name in an acs.\n\nDefault names\n\nname short name\nspecies S\ntransition T\naction A\nevent E\nparam P\nmeta M\n\nExamples\n\n@aka acs species=resource transition=reaction\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@mode","page":"API Documentation","title":"ReactiveDynamics.@mode","text":"Set species modality.\n\nSupported modalities\n\nnonblock\nconserved\nrate\n\nExamples\n\n@mode acs (r\"proj\\w+\", r\"experimental\\w+\") conserved\n@mode acs (S, I) conserved\n@mode acs S conserved\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@name_transition","page":"API Documentation","title":"ReactiveDynamics.@name_transition","text":"Set name of a transition in the model.\n\nExamples\n\n@name_transition acs 1=\"name\"\n@name_transition acs name=\"transition_name\"\n@name_transition acs \"name\"=\"transition_name\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Resource-costs","page":"API Documentation","title":"Resource costs","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@cost\n@valuation\n@reward","category":"page"},{"location":"index.html#ReactiveDynamics.@cost","page":"API Documentation","title":"ReactiveDynamics.@cost","text":"Set cost.\n\nExamples\n\n@cost model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@valuation","page":"API Documentation","title":"ReactiveDynamics.@valuation","text":"Set valuation.\n\nExamples\n\n@valuation model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@reward","page":"API Documentation","title":"ReactiveDynamics.@reward","text":"Set reward.\n\nExamples\n\n@reward model experimental1=2 experimental2=3\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Add-reactions","page":"API Documentation","title":"Add reactions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@push\n@jump\n@periodic","category":"page"},{"location":"index.html#ReactiveDynamics.@push","page":"API Documentation","title":"ReactiveDynamics.@push","text":"Add reactions to an acset.\n\nExamples\n\n@push sir_acs β*S*I*tdecay(@time()) S+I --> 2I name=>SI2I\n@push sir_acs begin \n    ν*I, I --> R, name=>I2R\n    γ, R --> S, name=>R2S\nend\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@jump","page":"API Documentation","title":"ReactiveDynamics.@jump","text":"Add a jump process (with specified Poisson intensity per unit time step) to a model.\n\nExamples\n\n@jump acs λ Z += rand(Poisson(1.))\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@periodic","page":"API Documentation","title":"ReactiveDynamics.@periodic","text":"Add a periodic callback to a model.\n\nExamples\n\n@periodic acs 1. X += 1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Set-initial-values,-uncertainty,-and-solver-arguments","page":"API Documentation","title":"Set initial values, uncertainty, and solver arguments","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@prob_init\n@prob_uncertainty\n@prob_params\n@prob_meta","category":"page"},{"location":"index.html#ReactiveDynamics.@prob_init","page":"API Documentation","title":"ReactiveDynamics.@prob_init","text":"Set initial values of species in an acset.\n\nExamples\n\n@prob_init acs X=1 Y=2 Z=h(α)\n@prob_init acs [1., 2., 3.]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_uncertainty","page":"API Documentation","title":"ReactiveDynamics.@prob_uncertainty","text":"Set uncertainty in initial values of species in an acset (stderr).\n\nExamples\n\n@prob_uncertainty acs X=.1 Y=.2\n@prob_uncertainty acs [.1, .2,]\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_params","page":"API Documentation","title":"ReactiveDynamics.@prob_params","text":"Set parameter values in an acset.\n\nExamples\n\n@prob_params acs α=1. β=2.\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@prob_meta","page":"API Documentation","title":"ReactiveDynamics.@prob_meta","text":"Set model metadata (e.g. solver arguments)\n\nExamples\n\n@prob_meta acs tspan=(0, 100.) schedule=schedule_weighted!\n@prob_meta sir_acs tspan=250 tstep=1\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-unions","page":"API Documentation","title":"Model unions","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@join\n@equalize","category":"page"},{"location":"index.html#ReactiveDynamics.@join","page":"API Documentation","title":"ReactiveDynamics.@join","text":"@join models... [equalize...]\n\nPerforms join of models and identifies model variables, as specified.\n\nModel variables / parameter values and metadata are propagated; the last model takes precedence.\n\nExamples\n\n@join acs1 acs2 @catchall(A)=acs2.Z @catchall(XY) @catchall(B)\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@equalize","page":"API Documentation","title":"ReactiveDynamics.@equalize","text":"Identify (collapse) a set of species in a model.\n\nExamples\n\n@join acs acs1.A=acs2.A B=C\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Model-import-and-export","page":"API Documentation","title":"Model import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_network\n@export_network","category":"page"},{"location":"index.html#ReactiveDynamics.@import_network","page":"API Documentation","title":"ReactiveDynamics.@import_network","text":"Import a model from a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@import_network \"model.toml\"\n@import_network \"csv/model.toml\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_network","page":"API Documentation","title":"ReactiveDynamics.@export_network","text":"Export model to a file: this can be either a single TOML file encoding the entire model, or a batch of CSV files (a root file and a number of files, each per a class of objects).\n\nSee tutorials/loadsave for an example.\n\nExamples\n\n@export_network acs \"acs_data.toml\" # as a TOML\n@export_network acs \"csv/model.csv\" # as a CSV\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Solution-import-and-export","page":"API Documentation","title":"Solution import and export","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@import_solution\n@export_solution_as_table\n@export_solution_as_csv\n@export_solution","category":"page"},{"location":"index.html#ReactiveDynamics.@import_solution","page":"API Documentation","title":"ReactiveDynamics.@import_solution","text":"@import_solution \"sol.jld2\"\n@import_solution \"sol.jld2\" sol\n\nImport a solution from a file.\n\nExamples\n\n@import_solution \"sir_acs_sol/serialized/sol.jld2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_table","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_table","text":"@export_solution_as_table sol\n\nExport a solution as a DataFrame.\n\nExamples\n\n@export_solution_as_table sol\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution_as_csv","page":"API Documentation","title":"ReactiveDynamics.@export_solution_as_csv","text":"@export_solution_as_csv sol\n@export_solution_as_csv sol \"sol.csv\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution_as_csv sol \"sol.csv\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@export_solution","page":"API Documentation","title":"ReactiveDynamics.@export_solution","text":"@export_solution sol\n@export_solution sol \"sol.jld2\"\n\nExport a solution to a file.\n\nExamples\n\n@export_solution sol \"sol.jdl2\"\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Problematize,sSolve,-and-plot","page":"API Documentation","title":"Problematize,sSolve, and plot","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@problematize\n@solve\n@plot","category":"page"},{"location":"index.html#ReactiveDynamics.@problematize","page":"API Documentation","title":"ReactiveDynamics.@problematize","text":"Convert a model to a DiscreteProblem. If passed a problem instance, return the instance.\n\nExamples\n\n@problematize acs tspan=1:100\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@solve","page":"API Documentation","title":"ReactiveDynamics.@solve","text":"Solve the problem. Solverargs passed at the calltime take precedence.\n\nExamples\n\n@solve prob\n@solve prob tspan=1:100\n@solve prob tspan=100 trajectories=20\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@plot","page":"API Documentation","title":"ReactiveDynamics.@plot","text":"Plot the solution (summary).\n\nExamples\n\n@plot sol plot_type=summary\n@plot sol plot_type=allocation # not supported for ensemble solutions!\n@plot sol plot_type=valuations # not supported for ensemble solutions!\n@plot sol plot_type=new_transitions # not supported for ensemble solutions!\n\n\n\n\n\n","category":"macro"},{"location":"index.html#Optimization-and-fitting","page":"API Documentation","title":"Optimization and fitting","text":"","category":"section"},{"location":"index.html","page":"API Documentation","title":"API Documentation","text":"@optimize\n@fit\n@fit_and_plot\n@build_solver","category":"page"},{"location":"index.html#ReactiveDynamics.@optimize","page":"API Documentation","title":"ReactiveDynamics.@optimize","text":"@optimize acset objective ... ... opts...\n\nTake an acset and optimize given functional.\n\nObjective is an expression which may reference the model's variables and parameters, i.e., A+β. The values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nBy default, the functional is minimized. Specify objective=max to perform maximization. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\n@optimize acs abs(A-B) A B=20. α=2. lower_bounds=0 upper_bounds=100\n@optimize acss abs(A-B) A B=20. α=2. upper_bounds=[200,300,400] maxeval=200 objective=min\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit","page":"API Documentation","title":"ReactiveDynamics.@fit","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset and fit initial values and parameters to empirical data.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@fit_and_plot","page":"API Documentation","title":"ReactiveDynamics.@fit_and_plot","text":"@fit acset data_points time_steps empiric_variables ... ... opts...\n\nTake an acset, fit initial values and parameters to empirical data, and plot the result.\n\nThe values to optimized are listed using their symbolic names; unless specified, the initial value is inferred from the model. The vector of free variables passed to the NLopt solver has the form [free_vars; free_params]; order of vars and params, respectively, is preserved. \n\nPropagates NLopt solver arguments; see NLopt documentation.\n\nExamples\n\nt = [1, 50, 100]\ndata = [80 30 20]\n@fit acs data t vars=A B=20 A α # fit B, A, α; empirical data is for variable A\n\n\n\n\n\n","category":"macro"},{"location":"index.html#ReactiveDynamics.@build_solver","page":"API Documentation","title":"ReactiveDynamics.@build_solver","text":"@build_solver acset ... ... opts...\n\nTake an acset and export a solution as a function of free vars and free parameters.\n\nExamples\n\nsolver = @build_solver acs S α β # function of variable S and parameters α, β\nsolver([S, α, β])\n\n\n\n\n\n","category":"macro"}]
 }
diff --git a/docs/src/index.md b/docs/src/index.md
index 87ec4e7..7104f87 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -28,7 +28,7 @@ We list common species attributes:
 
 Moreover, it is possible to specify the semantics of the "rate" term. By default, at each time step `n ~ Poisson(rate * dt)` instances of a given transition will be spawned. If you want to specify the rate in terms of a cycle time, you may want to use `@ct(cycle_time)`, e.g., `@ct(ex), A --> B, ...`. This is a shorthand for `1/ex, A --> B, ...`.
 
-For deterministic "rates", use `@per_step(ex)`. Here, `ex` evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover
+For deterministic "rates", use `@deterministic(ex)`. Here, `ex` evaluates to a deterministic number (ceiled to the nearest integer) of a transition's instances to spawn per a single integrator's step. However, note that in this case, the number doesn't scale with the step length! Moreover
 
 ```@docs
 @add_species
diff --git a/src/ReactiveDynamics.jl b/src/ReactiveDynamics.jl
index c10b7a0..7d4991d 100644
--- a/src/ReactiveDynamics.jl
+++ b/src/ReactiveDynamics.jl
@@ -104,7 +104,7 @@ Base.convert(::Type{FoldedObservable}, ex::String) = eval(Meta.parse(ex))
 prettynames = Dict(
     :transRate => [:rate],
     :specInitUncertainty => [:uncertainty, :stoch, :stochasticity],
-    :transPreAction => [:preAction, :pre],
+    :transPreAction => [:preAction, :action, :pre],
     :transPostAction => [:postAction, :post],
     :transName => [:name, :interpretation],
     :transPriority => [:priority],
diff --git a/src/compilers.jl b/src/compilers.jl
index eb651ba..49ecffd 100644
--- a/src/compilers.jl
+++ b/src/compilers.jl
@@ -64,8 +64,7 @@ function recursively_expand_dots_in_ex!(ex, vars)
     return ex
 end
 
-reserved_names =
-    [:t, :state, :obs, :resample, :solverarg, :take, :log, :periodic, :set_params]
+reserved_names = [:t, :obs, :resample, :solverarg, :take, :log, :periodic, :set_params]
 
 function escape_ref(ex, species)
     return if ex isa Symbol
@@ -92,20 +91,16 @@ function wrap_expr(fex, species_names, prm_names, varmap)
         let
         end
     )
+
     # expression walking (MacroTools): visit each expression, subsitute with the body's return value
     fex = prewalk(fex) do x
         # here we convert the query metalanguage: @t() -> time(state) etc. 
         if isexpr(x, :macrocall) && (macroname(x) ∈ reserved_names)
             Expr(:call, macroname(x), :state, x.args[3:end]...)
-        else
-            x
-        end
-    end
-
-    fex = prewalk(fex) do x
-        # here we convert the query metalanguage: @t() -> time(state) etc. 
-        if isexpr(x, :macrocall) && (macroname(x) == :transition)
+        elseif isexpr(x, :macrocall) && (macroname(x) == :transition)
             :transition
+        elseif isexpr(x, :macrocall) && (macroname(x) == :state)
+            :state
         else
             x
         end
@@ -148,7 +143,7 @@ function skip_compile(attr)
 end
 
 function compile_attrs(acs::ReactionNetworkSchema, structured_token)
-    species_names = setdiff(collect(acs[:, :specName]), structured_token)
+    species_names = collect(acs[:, :specName])#setdiff(collect(acs[:, :specName]), structured_token)
 
     prm_names = collect(acs[:, :prmName])
     varmap = Dict([name => :(state.u[$i]) for (i, name) in enumerate(species_names)])
diff --git a/src/interface/create.jl b/src/interface/create.jl
index 099ca1e..faf5bdf 100644
--- a/src/interface/create.jl
+++ b/src/interface/create.jl
@@ -146,7 +146,7 @@ function recursively_expand_actions!(evs, condex, event)
 end
 
 function expand_rate(rate)
-    rate = if !(isexpr(rate, :macrocall) && (macroname(rate) == :per_step))
+    rate = if !(isexpr(rate, :macrocall) && (macroname(rate) == :deterministic))
         :(rand(Poisson(max(state.dt * $rate, 0))))
     else
         rate.args[3]
@@ -271,7 +271,7 @@ function prune_reaction_line!(pcs, reactants, line)
     return line
 end
 
-function recursively_find_reactants!(reactants, pcs, ex::SampleableValues)
+function recursively_find_reactants!(reactants, pcs, ex)
     if typeof(ex) != Expr || isexpr(ex, :.) || (ex.head == :escape)
         if (ex == 0 || in(ex, empty_set))
             return :∅
@@ -293,8 +293,11 @@ function recursively_find_reactants!(reactants, pcs, ex::SampleableValues)
                 isexpr(ex.args[i], :tuple) ? ex.args[i].args[2] : ex.args[i],
             )
         end
+    elseif isexpr(ex, :macrocall) && macroname(ex) ∈ [:structured, :move]
+        return ex
     elseif isexpr(ex, :macrocall)
-        recursively_find_reactants!(reactants, pcs, ex.args[3])
+        pass_value = ex.args[3] isa QuoteNode ? ex.args[3].value : ex.args[3]
+        recursively_find_reactants!(reactants, pcs, pass_value)
     elseif isexpr(ex, :call)
         push!(reactants, ex.args[1])
     else
diff --git a/src/solvers.jl b/src/solvers.jl
index 9bf982c..46d3e6a 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -332,19 +332,31 @@ end
 
 function structured_rhs(expr::Expr, state, transition)
     if isexpr(expr, :macrocall) && macroname(expr) == :structured
-        expr = quote
-            token = $(expr.args[end-1])
-            species = $(expr.args[end])
+        if length(expr.args) == 3
+            expr = quote
+                return $(expr.args[end])
+            end
+            # write docs
+            token = context_eval(state, transition, state.wrap_fun(expr))
 
-            return token, species
-        end
+            entangle!(getagent(state, "structured"), token)
 
-        token, species = context_eval(state, transition, state.wrap_fun(expr))
-        set_species!(token, Symbol(species))
+            return token, get_species(token)
+        else
+            expr = quote
+                token = $(expr.args[end-1])
+                species = $(expr.args[end])
 
-        entangle!(getagent(state, "structured"), token)
+                return token, species
+            end
+            # write docs
+            token, species = context_eval(state, transition, state.wrap_fun(expr))
+            set_species!(token, Symbol(species))
 
-        return token, get_species(token)
+            entangle!(getagent(state, "structured"), token)
+
+            return token, get_species(token)
+        end
     elseif isexpr(expr, :macrocall) && macroname(expr) == :move
         expr = quote
             species_from = $(expr.args[end-1])
@@ -528,6 +540,8 @@ function ReactionNetworkProblem(
     structured_token_names =
         acs[filter(i -> acs[i, :specStructured], 1:nparts(acs, :S)), :specName]
 
+    println(acs[:, :specName])
+    println(structured_token_names)
     attrs, transitions, wrap_fun = compile_attrs(acs, structured_token_names)
     transition_recipes = transitions
     u0_init = zeros(nparts(acs, :S))
@@ -583,7 +597,7 @@ function ReactionNetworkProblem(
 
     entangle!(network, FreeAgent("structured"))
 
-    save!(network)
+    # save!(network)
 
     return network
 end
@@ -612,6 +626,11 @@ function update_u_structured!(state)
 end
 
 function AlgebraicAgents._step!(state::ReactionNetworkProblem)
+    update_u_structured!(state)
+    if isempty(state.sol)
+        save!(state)
+    end
+
     free_blocked_species!(state)
     update_u_structured!(state)
     update_observables(state)
@@ -632,8 +651,11 @@ function AlgebraicAgents._step!(state::ReactionNetworkProblem)
         ),
     )
 
+    state.t += state.dt
+
     save!(state)
-    return state.t += state.dt
+
+    return state.t
 end
 
 function AlgebraicAgents._projected_to(state::ReactionNetworkProblem)
diff --git a/tutorial/agents-integration/Project.toml b/tutorial/agents-integration/Project.toml
index ef794fc..dc9c60a 100644
--- a/tutorial/agents-integration/Project.toml
+++ b/tutorial/agents-integration/Project.toml
@@ -1,9 +1,16 @@
 [deps]
 AlgebraicAgents = "f6eb0ae3-10fa-40e6-88dd-9006ba45093a"
+CEEDesigns = "e939450b-799e-4198-a5f5-3f2f7fb1c671"
+Copulas = "ae264745-0b69-425e-9d9d-cf662c5eec93"
+DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
 Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
 Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
 JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
+MCTS = "e12ccd36-dcad-5f33-8774-9175229e7b33"
 MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
+POMDPTools = "7588e00f-9cae-40de-98dc-e0c70c48cdd7"
+POMDPs = "a93abf59-7444-517b-a68a-c42f96afdd7d"
 PlotGraphviz = "78a92bc3-407c-4e2f-aae5-75bb47a6fe36"
 ReactiveDynamics = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4"
+ScientificTypes = "321657f4-b219-11e9-178b-2701a2544e81"
 SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622"
diff --git a/tutorial/agents-integration/agents.jl b/tutorial/agents-integration/agents.jl
index b021720..6b6bc4d 100644
--- a/tutorial/agents-integration/agents.jl
+++ b/tutorial/agents-integration/agents.jl
@@ -24,7 +24,7 @@ network = @ReactionNetworkSchema
     ρ2, ∅ --> R2
 
     # Generate "Molecule 1" (where the integer corresponds to a "state" of, e.g., experimental triage).
-    ρ3, ∅ --> M1(@t(), rand(4))
+    ρ3, ∅ --> @structured(M1(@t(), rand(4)))
 
     # Based on properties of particular "structured agent" assigned to the transition,
     # we can update the attributes of the instance of a transition (such as probability of success).
@@ -33,28 +33,26 @@ network = @ReactionNetworkSchema
     # Update transition probability based on properties of "M1," 
     # which was assigned as a "resource" to the transition.
     ρ4,
-    R1 + M1 --> M2(@t(), rand(4)),
+    R1 + SM1 --> @structured(M2(@t(), rand(4))),
     preAction => update_prob_transition(state, transition)
 
-    # R2 + M1 --> M2(@t(), rand(4))
-    5.0, R2 + M1 --> @structured(M2(@t(), rand(4)), :A)
+    5.0, R2 + SM1 --> @structured(M2(@t(), rand(4)), :A)
     1.0, R2 + A --> @structured(M2(@t(), rand(4)), f_species(@transition))
 
-    1.0, R2 + M1 --> @move(M1, :M2)
-    1.0, R2 + M1 --> @move(M1, :C)
+    2.0, R2 + SM1 --> @move(:SM1, :C)
 end
 
 @prob_init network R1 = 10 R2 = 15
 
 # As for structured agents, we will need to instantiate the instances
 # and add them to the instance of a network. But first, we still need to define these types.
-@prob_init network M1 = 2 M2 = 0
+@prob_init network SM1 = 2 SM2 = 0
 
 @prob_params network ρ1 = 2 ρ2 = 1 ρ3 = 3 ρ4 = 4
 
 @prob_meta network tspan = 100 dt = 1.0
 
-# We use `@structured` macro, which is a convenience wrapper around `@aagent`),
+# We use `@structured` macro, which is a convenience wrapper around `@aagent`,
 # defined in ReactiveDynamics.jl
 @structured_token network struct M1
     descriptor::Any
@@ -68,7 +66,7 @@ using Random
 using ReactiveDynamics: M1
 
 function ReactiveDynamics.M1(time, descriptor)
-    return M1("M1" * randstring(4), :M1, nothing, [], descriptor, time)
+    return M1("M1" * randstring(4), :SM1, nothing, [], descriptor, time)
 end
 
 # We define the function which updates the transition probability.
@@ -94,12 +92,12 @@ end
         time_created::Any
     end
 
-    using Random
-    M2(time, descriptor) = M2("M2" * randstring(4), :M2, nothing, [], descriptor, time)
+    using Random: randstring
+    M2(time, descriptor) = M2("M2" * randstring(4), :SM2, nothing, [], descriptor, time)
 end
 
 # Let the network know that the species is structured.
-for species in [:M1, :M2, :A, :B, :C]
+for species in [:SM1, :SM2, :A, :B, :C]
     ReactiveDynamics.register_structured_species!(network, species)
 end
 
diff --git a/tutorial/agents-integration/ceed_integration.jl b/tutorial/agents-integration/ceed_integration.jl
new file mode 100644
index 0000000..18a29fe
--- /dev/null
+++ b/tutorial/agents-integration/ceed_integration.jl
@@ -0,0 +1,236 @@
+using CEEDesigns, CEEDesigns.GenerativeDesigns
+using DataFrames
+using ScientificTypes
+using Statistics, Copulas
+import POMDPs, POMDPTools, MCTS
+
+import Distributions
+
+# ----- Experimental Setup -----
+
+# We generate a synthetic dataset.
+# This is taken from https://github.com/Merck/CEEDesigns.jl/blob/34588ae0e5563cb93f6818e3a9c8b3a77c5e3c47/tutorials/SimpleGenerative.jl
+
+include("experimental_setup.jl")
+
+# ----- Get a sampling function -----
+
+(; sampler, uncertainty, weights) = DistanceBased(
+    data;
+    target = "y",
+    uncertainty = Variance(),
+    similarity = GenerativeDesigns.Exponential(; λ = 5),
+);
+
+# ----- Set up a reaction network -----
+
+#=
+Pkg.activate(".")
+Pkg.develop(path = "../..")
+=#
+
+using ReactiveDynamics
+
+# Set up parameters that will be used to define a network.
+
+# Experiments and costs
+features_experiments = Dict(["x$i" => "e$i" for i = 1:4])
+
+experiments_costs = Dict([
+    features_experiments[e] => (i, i) => [e] for (i, e) in enumerate(names(data)[1:4])
+])
+
+experiments_costs["ey"] = (100, 100) => ["y"]
+
+# Experimental resources
+experimental_resources = [:ER1, :ER2, :ER3]
+resources_quantities = [rand(1:3, length(experimental_resources)) for _ = 1:5]
+
+# "Compound," which is a structured token.
+# We use `@structured` macro, which is a convenience wrapper around `@aagent`,
+# defined in ReactiveDynamics.jl
+@register begin
+    @aagent BaseStructuredToken AbstractStructuredToken struct Compound
+        state::Any
+        history::Vector{Symbol}
+    end
+
+    get_cmpds = function (transition::Transition) end
+    run_experiment = function (agent::Compound, experiment::Symbol, rng) end
+    assign_to_places = function (state::ReactionNetworkProblem, threshold) end
+end
+
+# Provide a constructor for `Compound` and define functions that will
+# "execute" the experiments.
+import ReactiveDynamics: Compound, get_cmpds, assign_to_places, sample
+using ReactiveDynamics: ReactionNetworkProblem, Transition
+
+function Compound(id::AbstractString, predictions::Dict)
+    state = State((Evidence(predictions...), Tuple(zeros(2))))
+
+    return Compound("Compound $id", :pool, nothing, [], state, String[])
+end
+
+using Random: default_rng
+
+ReactiveDynamics.get_cmpds = function (transition::Transition)
+    if !isnothing(transition) && !isempty(transition.bound_structured_agents)
+        agent_ix = findall(x -> x isa Compound, transition.bound_structured_agents)
+
+        return transition.bound_structured_agents[agent_ix]
+    end
+end
+
+ReactiveDynamics.run_experiment =
+    function (agents::Vector, experiment::Symbol, rng = default_rng())
+        println("running experiment $experiment")
+        for agent in agents
+            push!(agent.history, experiment)
+            experiment = String(experiment)
+
+            observation = sampler(
+                agent.state.evidence,
+                getindex(experiments_costs[experiment], 2),
+                rng,
+            )
+
+            agent.state =
+                merge(agent.state, observation, first(experiments_costs[experiment]))
+        end
+        return agents
+    end
+
+transitions_experiments = String[]
+for i = 1:5
+    experiment = i < 5 ? "e$i" : "ey"
+
+    resource_part = join(
+        [
+            "$(resources_quantities[i][res_i]) * $res" for
+            (res_i, res) in enumerate(experimental_resources)
+        ],
+        " + ",
+    )
+
+    push!(
+        transitions_experiments,
+        """@deterministic($experiment), $experiment + $resource_part --> @move(:$experiment, :pool),
+            action => run_experiment(get_cmpds(@transition), :$experiment)
+        """,
+    )
+end
+
+# Set up a reaction network
+
+network = @ReactionNetworkSchema
+
+# Resource generation part
+@push network begin
+    p1, ∅ --> ER1
+    p2, ∅ --> ER2
+    p3, ∅ --> ER3
+end
+
+@prob_init network ER1 = 1200 ER2 = 1500 ER3 = 1300
+
+@prob_params network p1 = 1 p2 = 1 p3 = 1
+
+# Experiments part
+for species in union(Symbol.(keys(experiments_costs)), [:pool])
+    ReactiveDynamics.register_structured_species!(network, species)
+end
+
+str_network_def = """
+    begin
+        @push network begin
+            $(join(transitions_experiments, '\n'))
+        end
+    end 
+"""
+
+using MacroTools: striplines
+expr_network_def = striplines(Meta.parseall(str_network_def))
+
+eval(expr_network_def)
+
+@prob_meta network tspan = 100
+
+problem = ReactionNetworkProblem(network)
+
+using Random: randstring
+
+# Simplified setup: we assume that compounds are already assigned
+# to the "experimental" places.
+for _ = 1:10
+    cmpd = Compound(randstring(4), Dict())
+    cmpd.species = Symbol("e$(rand(1:4))")
+
+    add_structured_token!(problem, cmpd)
+end
+
+simulate(problem, 10)
+
+# To allow for dynamic assignment of compounds to places, we need to create an agent
+# that will move the agent to the corresponding places.
+# This can be expressed in two ways:
+# - Create an "algebraic agent" which will modify the reaction network's state,
+# - Create a "placeholder" transition which will run the mutating function.
+
+# In either case, we need to define the function that will facilitate the assignments.
+evidence = Evidence()
+
+solver = GenerativeDesigns.DPWSolver(; n_iterations = 500, tree_in_info = true)
+repetitions = 5
+mdp_options = (; max_parallel = 1, discount = 1.0, costs_tradeoff = (0.5, 0.5))
+
+ReactiveDynamics.assign_to_places =
+    function (state::ReactionNetworkProblem, threshold = 0.1)
+        compounds = filter(
+            x -> ReactiveDynamics.get_species(x) == :pool,
+            collect(values(inners(inners(state)["structured"]))),
+        )
+
+        for cmpd in compounds
+            e = get_next_experiment(cmpd.state.evidence, threshold)
+            if !isnothing(e)
+                cmpd.species = first(e)
+            end
+        end
+    end
+
+function get_next_experiment(evidence::Evidence, threshold = 0.1)
+    design = efficient_design(
+        experiments_costs;
+        sampler = sampler,
+        uncertainty = uncertainty,
+        threshold,
+        evidence = evidence,
+        solver = solver,
+        repetitions = repetitions,
+        mdp_options = mdp_options,
+    )
+
+    if !haskey(design[2], :arrangement) || isempty(design[2].arrangement)
+        return nothing
+    else
+        return first(design[2].arrangement)
+    end
+end
+
+@push network begin
+    @deterministic(2), ∅ --> @structured(Compound(randstring(4), Dict()))
+    @deterministic(1), ∅ --> ER1, preAction => assign_to_places(@state)
+end
+
+problem = ReactionNetworkProblem(network)
+
+# Simplified setup: we assume that compounds are already assigned
+# to the "experimental" places.
+for _ = 1:10
+    cmpd = Compound(randstring(4), Dict())
+    cmpd.species = Symbol("e$(rand(1:4))")
+
+    add_structured_token!(problem, cmpd)
+end
+
+simulate(problem, 10)
diff --git a/tutorial/agents-integration/experimental_setup.jl b/tutorial/agents-integration/experimental_setup.jl
new file mode 100644
index 0000000..cb5c4d0
--- /dev/null
+++ b/tutorial/agents-integration/experimental_setup.jl
@@ -0,0 +1,56 @@
+# This is taken from https://github.com/Merck/CEEDesigns.jl/blob/34588ae0e5563cb93f6818e3a9c8b3a77c5e3c47/tutorials/SimpleGenerative.jl
+
+make_friedman3 = function (U, noise = 0, friedman3 = true)
+    size(U, 2) == 4 || error("input U must have 4 columns, has $(size(U,2))")
+    n = size(U, 1)
+    X = DataFrame(zeros(Float64, n, 4), :auto)
+    for i = 1:4
+        X[:, i] .= U[:, i]
+    end
+    ϵ = noise > 0 ? rand(Distributions.Normal(0, noise), size(X, 1)) : 0
+    if friedman3
+        X.y = @. atan((X[:, 2] * X[:, 3] - 1 / (X[:, 2] * X[:, 4])) / X[:, 1]) + ϵ
+    else
+        ## Friedman #2
+        X.y = @. (X[:, 1]^2 + (X[:, 2] * X[:, 3] - 1 / (X[:, 2] * X[:, 4]))^2)^0.5 + ϵ
+    end
+    return X
+end
+
+p12, p13, p14, p23, p24, p34 = 0.8, 0.5, 0.3, 0.5, 0.25, 0.4
+Σ = [
+    1 p12 p13 p14
+    p12 1 p23 p24
+    p13 p23 1 p34
+    p14 p24 p34 1
+]
+
+X1 = Distributions.Uniform(0, 100)
+X2 = Distributions.Uniform(40 * π, 560 * π)
+X3 = Distributions.Uniform(0, 1)
+X4 = Distributions.Uniform(1, 11)
+
+C = GaussianCopula(Σ)
+D = SklarDist(C, (X1, X2, X3, X4))
+
+X = rand(D, 1000)
+
+data = make_friedman3(transpose(X), 0.01)
+
+data[1:10, :]
+
+# We can check that the empirical correlation is roughly the same as the specified theoretical values: 
+
+cor(Matrix(data[:, Not(:y)]))
+
+# We ensure that our algorithms know that we have provided data of specified types. 
+
+types = Dict(
+    :x1 => ScientificTypes.Continuous,
+    :x2 => ScientificTypes.Continuous,
+    :x3 => ScientificTypes.Continuous,
+    :x4 => ScientificTypes.Continuous,
+    :y => ScientificTypes.Continuous,
+)
+
+data = coerce(data, types);
diff --git a/tutorial/toy_pharma_model.jl b/tutorial/toy_pharma_model.jl
index e536d19..c308fe7 100644
--- a/tutorial/toy_pharma_model.jl
+++ b/tutorial/toy_pharma_model.jl
@@ -45,19 +45,19 @@ draw(sol)
 
 # model dynamics
 toy_pharma_model = @ReactionNetworkSchema begin
-    @per_step(α(candidate_compound, marketed_drug, κ)),
+    @deterministic(α(candidate_compound, marketed_drug, κ)),
     3 * @conserved(scientist) + @rate(budget) --> candidate_compound,
     name => discovery,
     probability => 0.3,
     cycletime => 10.0,
     priority => 0.5
-    @per_step(β(candidate_compound, marketed_drug)),
+    @deterministic(β(candidate_compound, marketed_drug)),
     candidate_compound + 5 * @conserved(scientist) + 2 * @rate(budget) -->
     marketed_drug + 5 * budget,
     name => dx2market,
     probability => 0.5 + 0.001 * @t(),
     cycletime => 4
-    @per_step(γ * marketed_drug), marketed_drug --> ∅, name => drug_killed
+    @deterministic(γ * marketed_drug), marketed_drug --> ∅, name => drug_killed
 end
 
 @periodic toy_pharma_model 0.0 budget += 11 * marketed_drug