Reactions (multi-species: species, coefficients, terms)#

Note

Engine: OpenSWMM 6 — refactored.

solver.reactions is the editable multi-species reaction system — the [REACTION_*] sections of a .rxn component file — as live objects: species with per-scope expressions, coefficients, intermediate terms, initial-quality rows, a whole-file text surface, and an engine-less static vocabulary for completers.

Reference: openswmm_reactions.h.


Quickstart#

from openswmm.engine import Solver, ReactionScope, ReactionExprForm

with Solver("model.inp") as s:
    k = s.reactions.coefficients.add("Kb", parameter=False, value=0.3)
    cl = s.reactions.species.add("CL2", units="MG", atol=1e-4, rtol=1e-4)

    cl.set_expression(ReactionScope.PIPE, ReactionExprForm.RATE, "-Kb*CL2")
    cl.set_expression(ReactionScope.TANK, ReactionExprForm.RATE, "-Kb*CL2")

    diag = s.reactions.validate("-Kb*CL2", ReactionScope.PIPE)
    print(diag.valid, diag.message, diag.column)

    text = s.reactions.serialize()      # canonical .rxn text
    s.reactions.apply_text(text)        # transactional round trip
    s.reactions.save("model.rxn")

Warning

Mutation is BUILDING/OPENED only — reaction rows seed state at initialize(), so there is nothing meaningful to change once a run is under way.


Eager validation: a stored model is never uncompilable#

Every mutator validates eagerly. The whole reaction system is recompiled before the call returns, and a mutation that would leave any expression uncompilable is rolled back and refused. There is no deferred check at run time and no “save now, discover later”: if the call succeeded, the system compiles.

Two consequences follow directly:

  • Removing a species, coefficient, or term that any compiled expression still references is refused. Rewrite the referring expressions first, then remove.

  • An editor can never store an uncompilable model, so it does not need a separate “is this model still valid?” pass after each keystroke.

Reactions.validate() is the read-only counterpart — compile-only, zero state change — returning an ExpressionDiagnostic named tuple of (valid, message, column). column is 1-based, or -1 when the diagnostic is not attributable to a position.

Note

Under ReactionScope.TERM, validation accepts references to all terms, including ones defined later. The forward-only ordering rule is enforced at file-apply time, where ordinal position exists. Validation answers “is this well-formed against the vocabulary”, not “is the file orderable”.


Species, coefficients, terms#

Collection

Contents

Reactions.species

ReactionSpecies — name, is_wall, units, atol, rtol, initial, and the two scope expressions.

Reactions.coefficients

ReactionCoefficient — name, is_param (PARAMETER vs CONSTANT), value.

Reactions.terms

ReactionTerm — name and its source expression.

Reactions.initial

ReactionInitialEntry rows — (is_link, elem_index, species_index, value).

All three collections support len(), iteration, in, indexing by int | str, get_index(key), add(...) and remove(key).

for sp in s.reactions.species:
    print(sp.index, sp.name, sp.units, sp.is_wall)

"CL2" in s.reactions.species          # True
s.reactions.species.get_index("CL2")  # 0

s.reactions.terms.add("Kf", "1.5826e-4*RE^0.88/D")

Per-scope expressions#

A species carries one expression per scope. Read them through ReactionSpecies.pipe_expression / ReactionSpecies.tank_expression, or generically through ReactionSpecies.get_expression(); both return a (ReactionExprForm, str) pair. ReactionSpecies.set_expression() writes one, and passing ReactionExprForm.NONE clears it.

form, expr = s.reactions.species["CL2"].get_expression(ReactionScope.PIPE)
s.reactions.species["CL2"].set_expression(
    ReactionScope.TANK, ReactionExprForm.NONE)

Options are canonical string tokens via Reactions.get_option() / Reactions.set_option(), keyed on SOLVER, COUPLING, RATE_UNITS, AREA_UNITS, TIMESTEP, ATOL and RTOL.


Whole-file text surface#

Reactions.serialize() renders canonical .rxn text from engine state; Reactions.apply_text() replaces the whole system from text; Reactions.check_text() is the dry run, with zero state change on success or failure. Reactions.save() writes to the component’s bound config path, or to an explicit path.

text = s.reactions.serialize()
diag = s.reactions.check_text(text)      # ExpressionDiagnostic
s.reactions.apply_text(text)
assert s.reactions.serialize() == text   # byte-identical

Two guarantees this surface makes:

  • Round trip. serialize() -> apply_text() -> serialize() is byte-identical. A text editor tab can hand the user its own output back without churn.

  • Transactional apply. Reactions.apply_text() is staged: on any error, the previous system — reaction state and registry block alike — is byte-identical to what it was before the call. A failed paste costs nothing.


Static vocabulary (completers)#

Reactions.hydraulic_variables() and Reactions.functions() are staticmethods: they need no engine and no open model, and are usable before anything is loaded.

from openswmm.engine import Reactions

for hv in Reactions.hydraulic_variables():
    print(hv.name, hv.description)        # ReactionHydVar

for fn in Reactions.functions():
    print(fn.name, fn.arity)              # ReactionFunction

Important

These two are the authoritative completer and syntax-highlighter vocabulary, sourced from the expression compiler’s own tables. Tooling must enumerate them rather than hard-code a list — that is what makes vocabulary drift structurally impossible instead of a matter of discipline. Combine them with the live Reactions.species / Reactions.coefficients / Reactions.terms names for the full identifier set.


See also#