Analysis & visualization

The read-only inspection layer over a finished run: per-token trajectory helpers, the ensemble runner and its summaries, treatment-effect comparisons across policy arms, the results-export bundle, and the three-layer network exec map. All operate over run state produced by ReactionNetworkProblem; none mutate the model.

ReactiveDynamics.token_trajectoryFunction
token_trajectory(state) -> DataFrame
token_trajectory(state, name::AbstractString) -> DataFrame
token_trajectory(state, pred::TokenPredicate) -> DataFrame

The per-token trajectory log in long form: columns t, program, species, <field>…, one row per (tick, opted-in token), in append (= token_sortkey-per-tick) order (§14.1). With a name it is one token's life; with a TokenPredicate (ADR 0008 / §9.5) it is the rows of the tokens that CURRENTLY match the predicate (the same selection machinery the model dynamics use — select_tokens), so e.g. token_trajectory(state, @select(Project, phase==:Market)) is the launched cohort's history. Heterogeneous field sets across kinds are unioned; an absent field is missing.

source
ReactiveDynamics.representative_tokenFunction
representative_token(state[, pred]) -> String

The MEDOID program (ADR 0013 §A5, sense (i) — "show me a typical program's life"): the token whose numeric logged path is closest to the per-cohort mean path, by per-field normalized distance. The cohort is all opted-in tokens, or those matching pred (a TokenPredicate). Returns the token name (index into token_trajectory(state, name)), or nothing if the cohort logged nothing. The metric is a documented heuristic (open question), uniform-weighted over numeric fields.

source
ReactiveDynamics.trajectory_envelopeFunction
trajectory_envelope(state[, pred]; align_on = :t) -> DataFrame

The ENVELOPE (ADR 0013 §A5, sense (ii) — "typical ± spread"): for each numeric logged field, the per-alignment-index median and inter-quartile band (q25/q75) across the cohort. The cohort is all opted-in tokens or those matching pred. With align_on = :t (default) tokens are aligned by absolute tick; pass another field symbol to align by an event index (e.g. ticks-since-first-row). Columns: align, field, median, q25, q75, n. This is also the single-run input to the ensemble band (ADR 0014 recipe 6). Pure post-processing over the store; adds no engine state.

source
ReactiveDynamics.ensembleFunction
ensemble(build; nseed, root_seed = 2026, max_t = nothing, parallel = false, mode = :rebuild) -> EnsembleProblem

Run nseed INDEPENDENT members of a scenario. build(seed) -> ReactionNetworkProblem constructs ONE member; member k ∈ 1:nseed is seeded hash((root_seed, k)) (§4 D8) and, if max_t is given, run to max_t (otherwise build is assumed to already simulate, as the demo's run_scenario does). Each member owns its own state.rng (no shared global RNG), so the result is order- and parallelism-independent (§4 D9) and (root_seed, nseed, build) fully determines the ensemble (Invariant 3).

Two member-production modes, recorded on the result as ens.mode:

  • mode = :rebuild (mode a, the default): build(seed) constructs a FRESH problem per member — robust because the structured token population is re-instantiated cleanly from the declarative population[] (ADR 0007 §B). Pays nseed × construction + closure-compilation cost.
  • mode = :reinit (mode b, reinit-reseed member reuse): build ONE member, then reinit-reseed and re-simulate that SAME problem for each subsequent seed (AlgebraicAgents.reinit!(m; seed), ADR 0007 §D + the reseed path), reusing its compiled closures and allocated store — the cheaper Monte Carlo path. After each member's run a faithful deepcopy SNAPSHOT of the finished problem is retained (so ens.members still holds nseed independent ReactionNetworkProblems the read surface + any metric(member) see unchanged); the reused problem is reinit-reseeded onward.

Mode-(a) ≡ mode-(b) equivalence + its precondition. Mode (b) is a legal substitute for mode (a) ONLY when members are structurally HOMOGENEOUS — same net, same population[] schema, differing only in the stochastic stream and the seed-sampled initial attributes. The reseed installs the new seed's stream BEFORE the t=0 marking is re-sampled (_reinit!), so a reseeded member is identical to a fresh build(seed) and the two modes produce the ensemble member-for-member. A build that BRANCHES STRUCTURALLY on its seed argument (a different net/population per seed) is out of contract for mode (b) and must use mode = :rebuild: mode (b) reseeds, it does NOT rebuild. This guard is documented, not auto-detected — build(root_seed) is called once for member 1 and reused thereafter.

parallel = true is accepted (members are independent) but currently runs sequentially; a threaded backend is future work (the API is fixed so callers need not change) — and it is only meaningful for :rebuild (mode (b) serially reuses one object). Subsumes demo/bd_acquisition/analysis.jl's hand-rolled ensemble.

source
ReactiveDynamics.summarizeFunction
summarize(ens, metric) -> NamedTuple

Reduce a per-run scalar metric(member::ReactionNetworkProblem) -> Real across the ensemble: (; mean, sem, q25, median, q75, n) (CONTRACT §14.2). The standard error of the mean is the honest spread the ensemble exists to report (sem = std/√n, 0 for n ≤ 1). Ledger statistics are e.g. summarize(ens, p -> last(program_ledger(p).valuation)).

source
ReactiveDynamics.treatment_effectFunction
treatment_effect(ens_baseline, ens_deal, metric) -> NamedTuple

The unpaired treatment effect on metric between two INDEPENDENT ensembles (CONTRACT §14.2): the difference of means with the unpaired SE se = sqrt(var_b/n_b + var_d/n_d) (the two arms desync the shared-RNG-free streams, so this is the unpaired estimator — MVP §4.1 finding A). Returns (; delta, se, baseline, deal, n_baseline, n_deal). This is the A/B lever comparison that drove the BD Δ-rNPV (treatment_effect(ensemble(baseline), ensemble(deal), rnpv)).

source
ReactiveDynamics.EnsembleProblemType
EnsembleProblem <: AbstractAlgebraicAgent

The result of ensemble — a FreeAgent-style container whose inners are the member ReactionNetworkProblems (entangled), plus the realized per-member seeds and the run mode. Because it implements the ADR 0012 read surface (observables/getobservable), an ensemble is itself a readable/drawable AA hierarchy node (CONTRACT §14.2 Invariant 4) — built ON AA's primitives, not by modifying AA. getobservable(ens, name) returns a cross-run reduction (see getobservable below).

source
ReactiveDynamics.export_runFunction
export_run(prob, dir; with_model = true) -> dir

Write the finished run prob to dir as the §14.3 bundle: trajectory.{csv,arrow} (prob.sol), ledger.{csv,arrow} (program_ledger), tokens.{csv,arrow} (the §14.1 token_trajectory long form, when any token opted in), events.json (the prob.log stream), tokens.json (per-token histories), and run.json (the manifest: model hash, seed, tspan, dt, schema version, and — when with_model — the embedded JSON model so the bundle is self-describing/replayable). Arrow siblings appear only when RDArrowExt is loaded (the user has Arrow); otherwise the CSV/JSON core is written alone. Returns dir.

source
ReactiveDynamics.export_ensembleFunction
export_ensemble(ens, dir; metric = nothing, with_model = true) -> dir

Write the ensemble ens to dir as the §14.3 layout: one subdirectory member_<k>/ per member (each an export_run bundle), plus a top-level ensemble.json recording the per-member seeds, the root seed, the run mode, and — when a metric::(member -> Real) is supplied — the summarize table over it. Returns dir.

source
ReactiveDynamics.network_graphFunction
network_graph(prob::ReactionNetworkProblem) -> NetworkGraph

Extract the Petri-net structure of a constructed model (Layer A). Walks the species table for the place nodes (flagging structured/agentic species) and the transition incidence for the arcs — today via transLHS/transRHS (the parsed reactant lists + the RHS expression). Because that incidence is realized by sample_transitions! (which draws stoichiometries through the RNG), this runs on a deepcopy of prob so the caller's state.rng is NOT perturbed — network_graph is observationally pure (no simulation, Invariant 1). The extraction simplifies (typed FKs, no reactant re-parse) when the ADR 0003 ReactantSpec table lands; this is the transLHS/transRHS form noted in §15.2.

source
ReactiveDynamics.to_graphvizFunction
to_graphviz(g::NetworkGraph; highlight_species = Symbol[], highlight_arcs = Tuple{Symbol,Symbol}[]) -> String

Emit Graphviz DOT for the Petri net (Layer B): species as circles, transitions as boxes, arcs with stoichiometry labels and color by §1 modality. highlight_species/highlight_arcs paint a subset (used by Layer C's overlay). Returns a DOT digraph STRING — rendering is deferred to draw_network (via AA's run_graphviz), so emitting the structure needs no Graphviz backend (Invariant 2). Valid DOT for any model; the smoke tests check dot accepts it for SIR/toy-pharma.

source
ReactiveDynamics.draw_networkFunction
draw_network(prob; format = "svg", prog = :dot, path = nothing, kwargs...)

Render the Petri net of prob (Layer B): builds the network_graph, emits DOT (to_graphviz, with any highlight_* kwargs forwarded), and renders it through AlgebraicAgents' run_graphviz (which uses Graphviz_jll if present, else a system dot). With path given, writes the rendered output there and returns the path; otherwise returns the rendered bytes as a String. Rendering is reuse, not reinvention — no new graph library (Invariant 2). If no Graphviz backend is available the DOT string is still obtainable via to_graphviz(network_graph(prob)).

source
ReactiveDynamics.exec_mapFunction
exec_map(prob; highlight = nothing, format = "svg", path = nothing, prog = :dot) -> String or path

The result-decorated "exec map" (Layer C / §15.2): the Petri net of prob with run statistics painted on — species nodes filled where their pool ran to a trough (starvation), and, when a highlight::TokenPredicate (a @select set, ADR 0008 / §9.5 — Invariant 4) is given, the matching cohort's past_bonds path THROUGH the net drawn as thickened arcs ("where did these programs go"). Decorates Layer A with finished-run statistics ONLY — it never mutates state or re-runs dynamics (Invariant 3). Returns the rendered output (or the path written to); the underlying DOT is always available via to_graphviz. The overlay is a styling pass over §14 data, not a new computation.

source
ReactiveDynamics.NetworkGraphType
NetworkGraph

A plain, inspectable Petri-net view of a model (ADR 0014 Layer A / §15.2 Invariant 1): species (place) nodes, transition nodes, and the arcs between them with stoichiometry + modality. Built by network_graph with NO plotting/Graphviz dependency and NO simulation — a pure function of the model — so the diagram is authoring-time documentation as well as a run artifact.

source

Plot specs

Typed plot specifications consumed by the RDPlotsExt recipes (loaded when Plots is present). Each wraps a run (or ensemble) and the variables to render.

ReactiveDynamics.MarkingPlotType
MarkingPlot(prob; vars = all species)

Plot spec (ADR 0014 recipe 1) for species/token COUNTS over time — the marking trajectory of the named vars across a finished run's prob.sol. Realized by a @recipe in RDPlotsExt; plot(MarkingPlot(prob)) needs Plots loaded.

source
ReactiveDynamics.SaturationPlotType
SaturationPlot(prob; vars = all species)

Plot spec (ADR 0014 recipe 2) for RESOURCE UTILIZATION over time — the troughs of the named resource pools vars across a finished run, showing when a @conserved/@rate resource is drawn down (saturated). Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.ValuationPlotType
ValuationPlot(prob)

Plot spec (ADR 0014 recipe 3) for the portfolio VALUATION curve — the cumulative cost/reward/valuation series the ledger logged over a finished run. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.LedgerPlotType
LedgerPlot(prob)

Plot spec (ADR 0014 recipe 4) for PER-PROGRAM cost/reward bars — the final per-program totals from program_ledger(prob), one bar group per program. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.TokenTrajectoryPlotType
TokenTrajectoryPlot(prob, field; pred = nothing)

Plot spec (ADR 0014 recipe 5) for one logged field's PER-TOKEN paths over time, overlaid with the cohort's typical envelope band (median + IQR, from trajectory_envelope). The cohort is all opted-in tokens, or those matching the TokenPredicate pred. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.EnsembleBarType
EnsembleBar(ens, metric)

Plot spec (ADR 0014 recipe 6a) for the DISTRIBUTION of a per-run scalar metric(member) -> Real across an ensemble's members — a histogram of the metric over the ensemble runs. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.TreatmentEffectPlotType
TreatmentEffectPlot(baseline, deal, metric)

Plot spec (ADR 0014 recipe 6b) for an A/B comparison: the metric distributions of the baseline and deal ensembles side by side, with the treatment effect Δ (see treatment_effect) annotated. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source
ReactiveDynamics.ThroughputPlotType
ThroughputPlot(prob)

Plot spec (ADR 0014 recipe 7) for THROUGHPUT over time — the number of transition firings and terminations per tick across a finished run. Realized by a @recipe in RDPlotsExt; needs Plots loaded.

source

The @export_solution_as_table / @export_solution_as_csv macros are the older, solution-output path predating the export_run bundle; prefer export_run/export_ensemble for new work.

AlgebraicAgents observable surface

ReactiveDynamics overloads the AlgebraicAgents read verbs for its problem and ensemble nodes, so a network's exported observables are readable by name (or canonical index) through the standard AA interface.

AlgebraicAgents.observablesFunction
observables(rd::ReactionNetworkProblem)

The ordered list of names this network exports to the AlgebraicAgents hierarchy (ADR 0012 §A, Invariant 1): every SPECIES name (net[:,:specName]) followed by every NAMED observable (keys(state.observables), §9.4). This is the canonical order getobservable(rd, i::Int) indexes.

Token aggregates are surfaced the LEAN-EXPLICIT way the ADR open question settles on: an author declares the aggregates worth exporting as NAMED observables (an observables[] entry, e.g. a count of Phase-2 tokens), which then appear here automatically — rather than auto-enumerating a combinatorial nactive × kind × phase set. Returns Vector{Symbol}.

source
observables(ens::EnsembleProblem)

The cross-run aggregate names an ensemble exports (ADR 0012 §A / §14.2 Invariant 4): the union of its members' observables, each surfaced as the across-member MEAN by getobservable. Stable, sorted order.

source
AlgebraicAgents.getobservableFunction
getobservable(rd::ReactionNetworkProblem, name)
getobservable(rd::ReactionNetworkProblem, i::Int)

The current value of an exported observable (ADR 0012 §A, Invariant 1). Reads are PURE and RNG-free — they never advance state.rng, so a coupled read does not perturb the trajectory:

  • a SPECIES count is state.u[idx(name)] (the live stock, structured or classical);
  • a NAMED observable is its last-sampled .sampled value (§9.4);
  • an Int indexes observables(rd) (the §A canonical order).

name may be a Symbol or a String (AA wires carry the from_var_name as a string through retrieve_input_vars, so both must resolve). An unknown name is a hard error — a diagnostic, never AA's silent @error fall-through (Invariant 1).

source
getobservable(ens::EnsembleProblem, name)

The across-member MEAN of member observable name (ADR 0012 §A / §14.2 Invariant 4): reads getobservable(member, name) on each member and averages. An unknown name is a hard error (a diagnostic, never AA's silent @error fall-through). Int indexes observables(ens).

source