Composition
The composition surface spans a granularity ladder: manual, no-port merges (merge_networks!/@join, equalize!/@equalize); declared open-port composition matched by foreign-key repoint (@compose/@pipeline/@process, compose, the @port/set_port_role!/port_role role surface); and boundary-matched refinement/abstraction (refine/refine!, abstract!/abstract_transitions), with refinement_diagnostics supplying advisory plug-compatibility checks. Composition and refinement are authoring-time-only operations — never apply them to a live, stepping model.
ReactiveDynamics.merge_networks! — Function
merge_networks!(net1, net2, name = gensym("net"), eqs = []) -> ReactionNetworkMerge net2 into net1 IN PLACE and return net1. net2 is deep-copied and its species namespaced under name (via prepend!) before merging, so the two fragments' private species stay distinct; a species already present in net1 is identified by name and its attribute cells overwritten from net2 (later fragment wins), with modality sets unioned. Transitions, params, metadata, events (:E) and observables (:obs) are all carried across — :E/:obs STRUCTURALLY (appended, never deduplicated). The eqs equation blocks drive species identification across fragments (see normalize_name). The engine behind @join.
ReactiveDynamics.@join — Macro
@join models... [equalize...]Performs join of models and identifies model variables, as specified.
Model variables / parameter values and metadata are propagated; the last model takes precedence.
Examples
@join net1 net2 @catchall(A) = net2.Z @catchall(XY) @catchall(B)ReactiveDynamics.equalize! — Function
equalize!(net, eqs = []) -> ReactionNetworkIdentify (collapse) sets of species in the static net, in place. Each block in eqs names species to merge — by exact :S index, by bare name, or by a :catchall name match (matching a __-namespaced suffix) — into a single surviving row aliased to the block's :alias (or its first entry). Missing attribute cells on the survivor are filled from the merged rows, every reference to a removed name is rewritten, and the removed rows are dropped by swap-and-pop; the promoted ReactantSpec table is then rebuilt so each reactant's species FK is repointed structurally onto the survivor (§7.4/J7, ADR 0003 Phase 2). Authoring-time only — FORBIDDEN on a live/stepping model, since it reindexes. @equalize is the declarative macro form.
ReactiveDynamics.@equalize — Macro
Identify (collapse) sets of species in a model — each equation names species to merge into one, so a downstream species can be fused with an upstream one (the FK-repoint of §7.4/J7, not string surgery).
Examples
@equalize net A = B C = DReactiveDynamics.refine — Function
refine(spec, transition, submodel; ports = Dict(boundary_species => sub_port, …)) -> ReactionNetworkNon-mutating convenience over refine!: splice submodel into the coarse transition of a deepcopy of spec, returning the refined copy and leaving spec untouched. Same port-matching semantics and authoring-time-only restriction as refine!.
ReactiveDynamics.refine! — Function
refine!(spec, transition, submodel; ports = Dict(boundary_species => sub_port, …))Splice submodel into the coarse transition (named Symbol) of spec, identifying each of the submodel's open ports (sub_port) with the parent boundary species (boundary_species) given in ports. Mutates and returns spec. Authoring-time only.
ReactiveDynamics.abstract_transitions — Function
abstract_transitions(spec, transitions, into; boundary)Inverse of refine!: collapse a connected set of sub-transitions (by name) into a single coarse transition named into, whose boundary reaction line consumes/produces the given boundary species. A structural convenience for round-tripping the granularity ladder; the collapsed coarse transition's attributes (cycletime/pos/cost) are the caller's to summarize (§C advises on drift).
ReactiveDynamics.abstract! — Function
abstract!(spec, transitions, into; lhs, rhs, attrs)Alias for abstract_transitions: collapse a connected set of sub-transitions into one coarse transition named into. The inverse of refine!.
ReactiveDynamics.set_port_role! — Function
set_port_role!(net, name => role, …)Set the open-port role ∈ (:private, :input, :output, :shared) (CONTRACT §11.1) of one or more species by name. :private (default) auto-namespaces on compose; :input/:output are open ports matched by @compose; :shared is identified by bare name (first-class @catchall).
ReactiveDynamics.port_role — Function
port_role(net, i::Integer) -> Symbol
port_role(net, name::Symbol) -> Union{Symbol, Nothing}The open-port role of a species (ADR 0009 §A / CONTRACT §11.1), one of :private, :input, :output, :shared. :private (the default for a species authored before roles existed or whose specRole cell is unset) is namespaced on compose; :input/:output are the open ports matched by @compose; :shared is identified by bare name. Indexed by row i, or by name (returning nothing if no such species). Set roles with set_port_role! / @port.
ReactiveDynamics.@port — Macro
@port net A => input B => input C => output clock => sharedDeclarative sugar for set_port_role!: tag species with a port role via species => role pairs (role ∈ input/output/shared; anything unlisted keeps its default :private). Each pair is written with => (not =, which macro-call syntax parses as a keyword argument).
ReactiveDynamics.@compose — Macro
@compose f1 f2 …Macro form of compose: compose declared-port model fragments (each an expression evaluating to a ReactionNetwork). The explicit-boundary counterpart of @join; @join/@equalize remain the manual no-declared-ports path.
ReactiveDynamics.@pipeline — Macro
@pipeline name begin
A => B : (ct=1.0, pos=0.4, res = 2*@conserved(scientist))
B => C : (ct=2.0, pos=0.6)
endExpand a phase chain into flow routing transitions (§2.8). Each From => To : (ct, pos, res) edge becomes a transition consuming From (+ optional res resources) and producing To, carrying the per-edge cycletime/probofsuccess. Returns a ReactionNetwork.
ReactiveDynamics.@process — Macro
@process name(params…) = begin <reaction lines> endDefine a reusable parameterized model-fragment factory. Expands to a function name(params…) that returns a ReactionNetwork. Inside the body, write ordinary reaction lines (as in @reaction_network); each occurrence of a PARAMETER name is substituted by its call-time value (a species symbol, a number, …) into the reaction-line AST BEFORE parsing — eval-free (replace_in_expr), sidestepping the DSL's lack of $-interpolation. Compose instances by ports with @compose (§E).
@process phase_gate(inp, outp; ct, pos) = begin
1.0, inp --> outp, name => g, cycletime => ct, probability => pos
end
gate_a = phase_gate(:Phase1, :Phase2; ct=2.0, pos=0.6)ReactiveDynamics.refinement_diagnostics — Function
refinement_diagnostics(spec, submodel, coarse_attrs; ports) -> Vector{String}Advisory §11.3 diagnostics for splicing submodel into a coarse transition described by coarse_attrs (a Dict of e.g. :transCycleTime, :transProbOfSuccess). Warnings only.
ReactiveDynamics.compose — Function
compose(fragments…; namespace=true)Compose model fragments by matching open ports. output ports are identified with same-named input ports across fragments (FK-repoint), shared species by bare name, private species are namespaced per fragment. Returns a new ReactionNetwork. @compose f1 f2 … is the macro form.
Namespacing internals
The merge operators namespace each fragment's private species before copying rows, so two fragments cannot conflate their state. These helpers are internal (unexported), documented here because the composition docstrings reference them.
ReactiveDynamics.prepend! — Function
Namespace net's species in place: rename each X → name__X and rewrite every reference to it across all attribute columns (and, structurally, inside observable option Exprs via prepend_obs!), so merging two fragments cannot conflate their private species. A :shared-role species (the first-class @catchall, ADR 0009 §A / CONTRACT §11.1) is identified by BARE name and left un-namespaced; :private (default) and the open :input/:output ports namespace here, with @compose re-identifying the open ports afterwards by FK-repoint. eqs drives cross-fragment identification via normalize_name. Called by merge_networks! before it copies rows across.
ReactiveDynamics.prepend_obs! — Function
Namespace the species referenced inside an observable's option expressions.
prepend! renames every species X → parent__X and records the map in specmap. An observable's sampling triggers (on) and range endpoints (range) are stored as Exprs inside a FoldedObservable (the :obsOpts column), which prepend!'s attribute loop skips — so without this the observable would still reference the pre-namespaced species and silently read the wrong (or a missing) pool after a join. This mirrors the per-attribute escape_ref + recursively_substitute_vars! rewrite prepend! applies to every other spec-referencing attribute. The observable's own NAME (obsName) is intentionally left un-namespaced — rate/guard exprs reference observables by bare name via @obs(x).
ReactiveDynamics.normalize_name — Function
The namespaced name for the i-th species (named name) of the fragment parent, honoring the identification blocks in eqs: if the species is named by a block — by exact :S index, by a :catchall name match, or by a parent-qualified name match — it collapses to that block's alias (its :alias entry, else a generated shared_species_N); otherwise it namespaces to parent__name. This is what lets @equalize/@join fuse species across fragments. Used by prepend!.