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..0ad533d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,28 +1,24 @@ name = "ReactiveDynamics" uuid = "c7456e7d-545a-4b79-91ea-6e93d96dd4d4" -version = "0.2.7" +version = "0.2.8" [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" -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" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -30,34 +26,27 @@ 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] -julia = "1.9" -DifferentialEquations = "7.9" -StatsFuns = "1.3" -Catlab = "0.14" -DataFrames = "1.6" -PlutoUI = "0.7" -Statistics = "1.9" -DocumenterMarkdown = "0.2" +ACSets = "0.2.6" +CSV = "0.10" ComponentArrays = "0.14" -JLD2 = "0.4" +Crayons = "4.1" +DataFrames = "1.6" +Distributions = "0.25" GeneratedExpressions = "0.1" -DiffEqBase = "6.128" -JSON = "0.21" -NLopt = "1.0" -OrdinaryDiffEq = "6.55" -Symbolics = "5.5" IJulia = "1.24" -SafeTestsets = "0.1" -CSV = "0.10" +JLD2 = "0.4" +JSON = "0.21" +MacroTools = "0.5" 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/docs/build/index.html b/docs/build/index.html index 93c5e9e..b340372 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)
@@ -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 addd92b..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 = @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 @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/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 083152f..7d4991d 100644
--- a/src/ReactiveDynamics.jl
+++ b/src/ReactiveDynamics.jl
@@ -1,9 +1,8 @@
 module ReactiveDynamics
 
-using Catlab, Catlab.CategoricalAlgebra, Catlab.Present
+using ACSets
 using Reexport
 using MacroTools
-using NLopt
 using ComponentArrays
 
 @reexport using GeneratedExpressions
@@ -28,60 +27,64 @@ 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,
+        :BoolAttributeT,
+    ], # AttrTypes
+    [
+        # species
+        (:specName, :S, :SymbolicAttributeT),
+        (:specModality, :S, :ModalityAttributeT),
+        (:specInitVal, :S, :SampleableAttributeT),
+        (:specInitUncertainty, :S, :SampleableAttributeT),
+        (:specCost, :S, :SampleableAttributeT),
+        (:specReward, :S, :SampleableAttributeT),
+        (:specValuation, :S, :SampleableAttributeT),
+        (:specStructured, :S, :BoolAttributeT),
+        # transitions
+        (:trans, :T, :SampleableAttributeT),
+        (:transPriority, :T, :SampleableAttributeT),
+        (:transRate, :T, :SampleableAttributeT),
+        (:transCycleTime, :T, :SampleableAttributeT),
+        (:transProbOfSuccess, :T, :SampleableAttributeT),
+        (:transCapacity, :T, :SampleableAttributeT),
+        (:transMaxLifeTime, :T, :SampleableAttributeT),
+        (:transPreAction, :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)
 
-const ReactionNetwork = FoldedReactionNetworkType{
+const ReactionNetworkSchema = FoldedReactionNetworkType{
     Symbol,
     Union{String,Symbol,Missing},
     SampleableValues,
     Set{Symbol},
     FoldedObservable,
     Any,
+    Bool,
 }
 
 Base.convert(::Type{Symbol}, ex::String) = Symbol(ex)
@@ -94,12 +97,14 @@ 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))
 
 prettynames = Dict(
     :transRate => [:rate],
     :specInitUncertainty => [:uncertainty, :stoch, :stochasticity],
+    :transPreAction => [:preAction, :action, :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,47 +132,43 @@ 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),
 )
 
 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)
+        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,
     )
 
     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)
@@ -195,7 +197,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,
@@ -220,7 +222,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..49ecffd 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
 
@@ -63,9 +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]
-push!(reserved_names, :state)
+reserved_names = [:t, :obs, :resample, :solverarg, :take, :log, :periodic, :set_params]
 
 function escape_ref(ex, species)
     return if ex isa Symbol
@@ -92,11 +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]...)
+        elseif isexpr(x, :macrocall) && (macroname(x) == :transition)
+            :transition
+        elseif isexpr(x, :macrocall) && (macroname(x) == :state)
+            :state
         else
             x
         end
@@ -112,12 +116,17 @@ 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 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)
+            return $letex
+        end
+    end)
 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,8 +142,9 @@ function skip_compile(attr)
            (string(attr) == "trans")
 end
 
-function compile_attrs(acs::ReactionNetwork)
-    species_names = collect(acs[:, :specName])
+function compile_attrs(acs::ReactionNetworkSchema, 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)])
     for name in prm_names
@@ -164,12 +174,12 @@ function compile_attrs(acs::ReactionNetwork)
     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
 
-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/agents.jl b/src/interface/agents.jl
new file mode 100644
index 0000000..fcd5558
--- /dev/null
+++ b/src/interface/agents.jl
@@ -0,0 +1,71 @@
+export AbstractStructuredToken, BaseStructuredToken
+export @structured_token
+export add_structured_token!
+
+# Abstract supertype of all structured species.
+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 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.
+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_token(network, type)
+    quote
+        $(AlgebraicAgents.aagent(
+            BaseStructuredToken,
+            AbstractStructuredToken,
+            type,
+            ReactiveDynamics,
+        ))
+    end
+end
+
+# Add a structured agent instance to an instance of a reaction network.
+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(::AbstractStructuredToken) = nothing
+AlgebraicAgents._step!(::AbstractStructuredToken) = nothing
+
+# Tell if an agent is assigned to a transition, as a resource.
+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::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/create.jl b/src/interface/create.jl
index 1a82374..faf5bdf 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 ###
@@ -146,8 +146,8 @@ function recursively_expand_actions!(evs, condex, event)
 end
 
 function expand_rate(rate)
-    rate = if !(isexpr(rate, :macrocall) && (macroname(rate) == :per_step))
-        :(rand(Poisson(max($rate, 0))))
+    rate = if !(isexpr(rate, :macrocall) && (macroname(rate) == :deterministic))
+        :(rand(Poisson(max(state.dt * $rate, 0))))
     else
         rate.args[3]
     end
@@ -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,13 @@ 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
         push!(reactants, underscorize(ex))
     end
diff --git a/src/interface/plots.jl b/src/interface/plots.jl
new file mode 100644
index 0000000..23892e0
--- /dev/null
+++ b/src/interface/plots.jl
@@ -0,0 +1,31 @@
+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, :)
+
+    return 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
+    return p
+end
diff --git a/src/interface/reaction_parser.jl b/src/interface/reaction_parser.jl
index ae92ae3..1f83113 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
@@ -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::ReactionNetworkProblem)
     r_line = recursively_choose(r_line, state)
 
     return recursive_find_reactants!(
@@ -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))
diff --git a/src/interface/solve.jl b/src/interface/solve.jl
index a7484e9..e38956c 100644
--- a/src/interface/solve.jl
+++ b/src/interface/solve.jl
@@ -1,64 +1,8 @@
-export @problematize, @solve, @plot
-export @optimize, @fit, @fit_and_plot, @build_solver
+export @agentize
 
-using DifferentialEquations: DiscreteProblem, EnsembleProblem, FunctionMap, EnsembleSolution
 import MacroTools
 import Plots
 
-"""
-Convert a model to a `DiscreteProblem`. If passed a problem instance, return the instance.
-
-# Examples
-
-```julia
-@problematize acs tspan = 1:100
-```
-"""
-macro problematize(acsex, args...)
-    args, kwargs = args_kwargs(args)
-    quote
-        if $(esc(acsex)) isa DiscreteProblem
-            $(esc(acsex))
-        else
-            DiscreteProblem($(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 trajectories = 20
-```
-"""
-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 DiscreteProblem
-            $(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)
-        end
-    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!"
@@ -255,248 +199,3 @@ function plot_from_log(state, record_type, ixs; kwargs...)
         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
diff --git a/src/interface/update.jl b/src/interface/update.jl
index 326c3f6..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
@@ -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/loadsave.jl b/src/loadsave.jl
index da3d8b4..4002a24 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,11 +24,11 @@ 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 => [])
-        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::ReactionNetwork)
 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
@@ -113,7 +116,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 = [])
@@ -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 cbe357d..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)
@@ -21,13 +21,13 @@ 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)
         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]) ||
                 (
@@ -53,9 +53,10 @@ function equalize!(acs::ReactionNetwork, 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 40ebc73..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 = 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))
-        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(
@@ -41,13 +43,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])
@@ -59,9 +61,9 @@ 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)
+    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
@@ -69,10 +71,10 @@ function prepend!(acs::ReactionNetwork, 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
 
@@ -199,7 +201,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 e2f22f1..46d3e6a 100644
--- a/src/solvers.jl
+++ b/src/solvers.jl
@@ -1,9 +1,7 @@
-# assortment of SciML-compatible problem solvers
-
-export DiscreteProblem
-
-using DiffEqBase, DifferentialEquations
 using Distributions
+using Random
+
+export ReactionNetworkProblem
 
 function get_sampled_transition(state, i)
     transition = Dict{Symbol,Any}()
@@ -17,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)
@@ -32,11 +30,16 @@ 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.solverargs[:tstep])
+                (reqs[tok.index, i] += qs[i] * tok.stoich * state.dt)
+            if in(:rate, tok.modality) && in(tok.species, state.structured_token)
+                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
@@ -57,7 +60,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))
@@ -71,7 +74,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]
@@ -99,21 +102,25 @@ 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]) &&
                 (reqs[tok.index, i] += tok.stoich)
         end
     end
+
     for i in eachindex(allocs)
         allocs[i] = reqs[i] == 0.0 ? Inf : floor(allocs[i] / reqs[i])
     end
+
     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))
 
@@ -123,9 +130,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))
@@ -133,11 +139,12 @@ function evolve!(u, 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)
-        new_instances = state.solverargs[:tstep] * qs[i] + state[i, :transToSpawn]
+
+    for i in parts(state, :T)
+        new_instances = qs[i] + state[i, :transToSpawn]
         capacity =
             state[i, :transCapacity] -
             count(t -> t[:transHash] == state[i, :transHash], state.ongoing_transitions)
@@ -147,8 +154,9 @@ function evolve!(u, state)
     end
 
     reqs = get_reqs_init!(reqs, qs, state)
-    allocs =
-        get_allocs!(reqs, u, state, state[:, :transPriority], state.solverargs[:strategy])
+
+    allocs = get_allocs!(reqs, state.u, state, state[:, :transPriority], state.p[:strategy])
+
     qs .= get_init_satisfied(allocs, qs, state)
 
     push!(
@@ -159,18 +167,67 @@ 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)
 
+    structured_token = collect(values(inners(getagent(state, "structured"))))
+
     # add spawned transitions to the heap
-    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),
-        )
+    for i in parts(state, :T)
+        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
+            structured_to_agents = transition.structured_to_agents
+
+            for (j, type) in enumerate(state.acs[:, :specName])
+                if type ∈ state.structured_token
+                    if !isinteger(allocs[j, i])
+                        error(
+                            "For structured species, stoichiometry coefficient must be integer in transition $i.",
+                        )
+                    end
+
+                    available_species = filter(
+                        a -> get_species(a) == type && !isblocked(a),
+                        structured_token,
+                    )
+
+                    sort!(
+                        available_species;
+                        by = a -> priority(a, state.acs[i, :transName]),
+                        rev = true,
+                    )
+
+                    ix = 1
+                    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
+                end
+            end
+
+            context_eval(state, transition, state.wrap_fun(state.acs[i, :transPreAction]))
+        end
     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)
@@ -178,10 +235,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!(
@@ -195,13 +252,54 @@ 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],
-        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
+            structured_to_agents = transition.structured_to_agents
+
+            for (j, type) in enumerate(state.acs[:, :specName])
+                if type ∈ state.structured_token
+                    if !isinteger(allocs[j, i])
+                        error(
+                            "For structured species, stoichiometry coefficient must be integer in transition $i.",
+                        )
+                    end
+
+                    available_species = filter(
+                        a -> get_species(a) == type && !isblocked(a),
+                        structured_token,
+                    )
+
+                    sort!(
+                        available_species;
+                        by = a -> priority(a, state.acs[i, :transName]),
+                        rev = true,
+                    )
+
+                    ix = 1
+                    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
+                end
+            end
+        end
+    end
 
     push!(state.log, (:allocation, state.t, actual_allocs))
     return push!(
@@ -209,14 +307,14 @@ function evolve!(u, 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]
@@ -227,9 +325,78 @@ 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
+        if length(expr.args) == 3
+            expr = quote
+                return $(expr.args[end])
+            end
+            # write docs
+            token = context_eval(state, transition, state.wrap_fun(expr))
+
+            entangle!(getagent(state, "structured"), token)
+
+            return token, get_species(token)
+        else
+            expr = quote
+                token = $(expr.args[end-1])
+                species = $(expr.args[end])
+
+                return token, species
+            end
+            # write docs
+            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)
+        end
+    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!(u, state)
-    update_u!(state, u)
+function finish!(state)
     val_reward = 0
     terminated_all = Dict{Symbol,Float64}()
     terminated_success = Dict{Symbol,Float64}()
@@ -238,46 +405,79 @@ function finish!(u, 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
+                stoich = context_eval(state, trans_, state.wrap_fun(r.stoich))
+
+                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)
+                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) && (
-                u[tok.index] +=
+            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_token
+                    for _ = 1:(trans_.q*tok.stoich)
+                        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
+            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_token
+                    for _ = 1:(trans_.q*tok.stoich)
+                        set_bound_transition!(
+                            trans_.nonblock_structured_agents[begin].bound_transition,
+                            nothing,
+                        )
+                        deleteat!(trans_.nonblock_structured_agents, 1)
+                    end
+                end
+            end
         end
-        foreach(
-            tok -> (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
-        terminated_success[trans_[:transHash]] =
-            get(terminated_success, trans_[:transHash], 0) + q
+
+        context_eval(state, trans_, state.wrap_fun(state.acs[trans_.i, :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
 
         ix += 1
     end
@@ -288,61 +488,21 @@ 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)
     for trans in state.ongoing_transitions, tok in trans[:transLHS]
         in(:nonblock, tok.modality) && (state.u[tok.index] += q * tok.stoich)
     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::ReactiveDynamicsState;
-    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)
+    for trans in state.ongoing_transitions
+        for a in trans.nonblock_structured_agents
+            a.bound_transition = nothing
+        end
 
-        return du
+        empty!(trans.nonblock_structured_agents)
     end
-
-    return DiffEqBase.DiscreteProblem(
-        f,
-        state.u,
-        (0.0, 2.0),
-        Dict(state.p..., :__state__ => state, :__state0__ => deepcopy(state));
-        kwargs...,
-    )
 end
 
 ## resolve tspan, tstep
@@ -352,106 +512,159 @@ 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
 
-"""
-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(
-    acs::ReactionNetwork,
+function ReactionNetworkProblem(
+    acs::ReactionNetworkSchema,
     u0 = Dict(),
-    p = DiffEqBase.NullParameters();
+    p = Dict();
+    name = "reaction_network",
     kwargs...,
 )
     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)))
     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)
-    state = ReactiveDynamicsState(
+
+    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))
+
+    for i in parts(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
+
+    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)
+    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)
+
+    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))),
+        keywords[:tspan][1],
+        structured_token_names,
+        keywords[:tspan],
+        get(keywords, :tstep, 1),
         transitions,
+        ongoing_transitions,
+        log,
+        observables,
         wrap_fun,
-        keywords[:tspan][1];
-        keywords...,
+        sol,
     )
-    init_u!(state)
-    save!(state)
 
-    prob = transform(DiffEqBase.DiscreteProblem, state; kwargs...)
-
-    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,
-        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),
-            ),
-        ),
-    )
+    entangle!(network, FreeAgent("structured"))
 
-    return prob
-end
+    # save!(network)
 
-function fetch_params(acs::ReactionNetwork)
-    return Dict{Symbol,Any}((
-        acs[i, :prmName] => acs[i, :prmVal] for
-        i in Iterators.filter(i -> !isnothing(acs[i, :prmVal]), 1:nparts(acs, :P))
-    ))
+    return network
 end
 
-# EnsembleProblem's prob_func: sample initial values
-function get_prob_func(prob)
-    vars = prob.p[:__state__][:, :specInitUncertainty]
+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)
 
-    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
+    return state
+end
+
+function update_u_structured!(state)
+    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
-        sync!(prob.p[:__state__], prob.u0, prob.p)
+    end
+
+    return state.u
+end
 
-        return prob
+function AlgebraicAgents._step!(state::ReactionNetworkProblem)
+    update_u_structured!(state)
+    if isempty(state.sol)
+        save!(state)
     end
 
-    return prob_func
+    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)
+
+    push!(
+        state.log,
+        (
+            :valuation,
+            state.t,
+            state.u' * [state[i, :specValuation] for i in parts(state, :S)],
+        ),
+    )
+
+    state.t += state.dt
+
+    save!(state)
+
+    return state.t
+end
+
+function AlgebraicAgents._projected_to(state::ReactionNetworkProblem)
+    return state.t > state.tspan[2] ? true : state.t
+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]), parts(acs, :P))
+    ))
 end
diff --git a/src/state.jl b/src/state.jl
index 9567317..d0eef41 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -1,4 +1,5 @@
-using DiffEqBase: NullParameters
+@reexport using AlgebraicAgents
+using DataFrames
 
 struct UnfoldedReactant
     index::Int
@@ -10,17 +11,24 @@ end
 """
 Ongoing transition auxiliary structure.
 """
-mutable struct Transition
+@aagent struct Transition
+    i::Int
+
     trans::Dict{Symbol,Any}
 
+    bound_structured_agents::Vector{AbstractAlgebraicAgent}
+    nonblock_structured_agents::Vector{AbstractAlgebraicAgent}
+    structured_to_agents::Vector
+
     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
 
-mutable struct Observable
+@aagent struct Observable
     last::Float64 # last sampling time
     range::Vector{Union{Tuple{Float64,SampleableValues},SampleableValues}}
     every::Float64
@@ -29,8 +37,8 @@ mutable struct Observable
     sampled::Any
 end
 
-mutable struct ReactiveDynamicsState
-    acs::ReactionNetwork
+@aagent struct ReactionNetworkProblem
+    acs::ReactionNetworkSchema
 
     attrs::Dict{Symbol,Vector}
     transition_recipes::Dict{Symbol,Vector}
@@ -39,79 +47,49 @@ mutable struct ReactiveDynamicsState
     p::Any
     t::Float64
 
+    structured_token::Vector{Symbol}
+
+    tspan::Tuple{Float64,Float64}
+    dt::Float64
+
     transitions::Dict{Symbol,Vector}
     ongoing_transitions::Vector{Transition}
     log::Vector{Tuple}
 
     observables::Dict{Symbol,Observable}
-    solverargs::Any
 
     wrap_fun::Any
-    history_u::Vector{Vector{Float64}}
-    history_t::Vector{Float64}
-
-    function ReactiveDynamicsState(
-        acs::ReactionNetwork,
-        attrs,
-        transition_recipes,
-        wrap_fun,
-        t0 = 0;
-        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 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
+    sol::DataFrame
 end
 
 # get value of a numeric expression
 # evaluate compiled numeric expression in context of (u, p, t)
-function context_eval(state::ReactiveDynamicsState, 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::ReactiveDynamicsState, keys...)
-    return context_eval(
-        state,
-        (contains(string(keys[2]), "trans") ? state.transitions : state.attrs)[keys[2]][keys[1]],
-    )
+function Base.getindex(state::ReactionNetworkProblem, keys...)
+    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::ReactiveDynamicsState)
+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
-function save!(state::ReactiveDynamicsState)
-    return (push!(state.history_u, state.u); push!(state.history_t, state.t))
-end
+save!(state::ReactionNetworkProblem) = push!(state.sol, (state.t, state.u[:]...))
 
-function compile_observables(acs::ReactionNetwork)
+function compile_observables(acs::ReactionNetworkSchema)
     observables = Dict{Symbol,Observable}()
     species_names = collect(acs[:, :specName])
     prm_names = collect(acs[:, :prmName])
@@ -127,7 +105,10 @@ function compile_observables(acs::ReactionNetwork)
             opts.range,
         )
 
-        push!(observables, name => Observable(-Inf, range, opts.every, on, missing))
+        push!(
+            observables,
+            name => Observable(string(name), -Inf, range, opts.every, on, missing),
+        )
     end
 
     return observables
@@ -149,16 +130,16 @@ function sample_range(rng, state)
     return r isa Sampleable ? rand(r) : r
 end
 
-function resample!(state::ReactiveDynamicsState, 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))
+    return o.sampled = context_eval(state, nothing, sample_range(o.range, state))
 end
 
-resample(state::ReactiveDynamicsState, o::Symbol) = resample!(state, state.observables[o])
+resample(state::ReactionNetworkProblem, o::Symbol) = resample!(state, state.observables[o])
 
-function update_observables(state::ReactiveDynamicsState)
+function update_observables(state::ReactionNetworkProblem)
     return foreach(
         o -> (state.t - o.last) >= o.every && resample!(state, o),
         values(state.observables),
@@ -167,7 +148,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)
@@ -184,11 +165,11 @@ function prune_r_line(r_line)
     end
 end
 
-function find_index(species::Symbol, state::ReactiveDynamicsState)
-    return findfirst(i -> state[i, :specName] == species, 1:nparts(state, :S))
+function find_index(species::Symbol, state::ReactionNetworkProblem)
+    return findfirst(i -> state[i, :specName] == species, parts(state, :S))
 end
 
-function sample_transitions!(state::ReactiveDynamicsState)
+function sample_transitions!(state::ReactionNetworkProblem)
     for (_, v) in state.transitions
         empty!(v)
     end
@@ -197,10 +178,13 @@ function sample_transitions!(state::ReactiveDynamicsState)
         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
 
@@ -212,62 +196,57 @@ function sample_transitions!(state::ReactiveDynamicsState)
                 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
 
-## 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)
-
-function sync!(state::ReactiveDynamicsState, u, p)
-    state.u .= u
-    for k in keys(state.p)
-        haskey(p, k) && (state.p[k] = p[k])
-    end
+function as_state(u, t, state::ReactionNetworkProblem)
+    return (state = deepcopy(state); state.u .= u; state.t = t; state)
 end
 
-function as_state(u, t, state::ReactiveDynamicsState)
-    return (state = deepcopy(state); state.u .= u; state.t = t; state)
+function ACSets.ACSetInterface.nparts(state::ReactionNetworkProblem, obj::Symbol)
+    return nparts(state.acs, obj)
 end
 
-function Catlab.CategoricalAlgebra.nparts(state::ReactiveDynamicsState, obj::Symbol)
-    return obj == :T ? length(state.transitions[:transLHS]) : nparts(state.acs, obj)
+function ACSets.ACSetInterface.parts(state::ReactionNetworkProblem, obj::Symbol)
+    return parts(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::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::ReactiveDynamicsState, 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)
+        length(state.sol.t) > 1 &&
+        (fld(state.t, period) - fld(state.sol.t[end-1], period) > 0)
     )
 end
 
-set_params(state::ReactiveDynamicsState, vals...) =
+set_params(state::ReactionNetworkProblem, vals...) =
     for (p, v) in vals
         state.p[p] = v
     end
 
-function add_to_spawn!(state::ReactiveDynamicsState, 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
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/test/tutorial_tests.jl b/test/tutorial_tests.jl
index 8ff8b8f..5f55109 100644
--- a/test/tutorial_tests.jl
+++ b/test/tutorial_tests.jl
@@ -1,8 +1,10 @@
 using ReactiveDynamics
 
+include("safeinclude.jl")
+
 @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/agents-integration/Project.toml b/tutorial/agents-integration/Project.toml
new file mode 100644
index 0000000..dc9c60a
--- /dev/null
+++ b/tutorial/agents-integration/Project.toml
@@ -0,0 +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
new file mode 100644
index 0000000..6b6bc4d
--- /dev/null
+++ b/tutorial/agents-integration/agents.jl
@@ -0,0 +1,116 @@
+# --------------------------------------------------------------------------------
+# 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, ∅ --> @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).
+
+    # 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 + SM1 --> @structured(M2(@t(), rand(4))),
+    preAction => update_prob_transition(state, transition)
+
+    5.0, R2 + SM1 --> @structured(M2(@t(), rand(4)), :A)
+    1.0, R2 + A --> @structured(M2(@t(), rand(4)), f_species(@transition))
+
+    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 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`,
+# defined in ReactiveDynamics.jl
+@structured_token network struct M1
+    descriptor::Any
+    time_created::Any
+end
+
+using Random
+
+# Type `M1` lives in the scope of ReactiveDynamics.
+# Accordingly, we have to explicitly declare the scope.
+using ReactiveDynamics: M1
+
+function ReactiveDynamics.M1(time, descriptor)
+    return M1("M1" * randstring(4), :SM1, 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.
+@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] = 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 BaseStructuredToken AbstractStructuredToken struct M2
+        descriptor::Any
+        time_created::Any
+    end
+
+    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 [:SM1, :SM2, :A, :B, :C]
+    ReactiveDynamics.register_structured_species!(network, species)
+end
+
+# --------------------------------------------------------------------------------
+# Instantiate the network.
+network_instance = ReactionNetworkProblem(network)
+
+for i = 1:2
+    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"))))
diff --git a/tutorial/agents-integration/agents_integration.jl b/tutorial/agents-integration/agents_integration.jl
new file mode 100644
index 0000000..3279902
--- /dev/null
+++ b/tutorial/agents-integration/agents_integration.jl
@@ -0,0 +1,366 @@
+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.0, 1.0);
+
+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 = 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 = 1:length(experimental_resources)], " ") *
+    " " *
+    join(["p_primary_$i = $(rand(1:5))" for i = 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);
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/basics.jl b/tutorial/basics.jl
index 3b0267e..bc1ce13 100644
--- a/tutorial/basics.jl
+++ b/tutorial/basics.jl
@@ -1,23 +1,22 @@
 using ReactiveDynamics
 
-# acs = @ReactionNetwork begin
-#     1.0, X ⟺ Y
-# end
-
-acs = @ReactionNetwork begin
-    1.0, X ⟺ Y, name => "transition1"
+# 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.1
-
-prob = @problematize acs
+@prob_meta acs tspan = 25 dt = 0.10
 
-# sol = ReactiveDynamics.solve(prob)
+# convert network into an AlgAgents hierarchy
+prob = ReactionNetworkProblem(acs)
 
-sol = @solve prob trajectories = 20
+# simulate
+simulate(prob)
 
-using Plots
+# access solution
+prob.sol
 
-@plot sol plot_type = summary
+# plot solution
+draw(prob)
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..c308fe7 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,33 +35,29 @@ 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
-    @per_step(α(candidate_compound, marketed_drug, κ)),
+toy_pharma_model = @ReactionNetworkSchema begin
+    @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
@@ -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)