Engine catalog#

openswmm.engine.catalog is a machine-readable map of the Python API, shipped in the wheel as catalog.json. It lists every service, element kind, sub-view and standalone class, and for each member:

  • a property’s type, access (r or rw), unit kind and the lifecycle phases it can be read in;

  • a method’s parameters, defaults and return type;

  • the C API functions behind it;

  • the bulk array that reads a whole collection at once, where there is one.

The OpenSWMM MCP server and openswmm.gymnasium use it to reach engine fields and methods by path. Existing generic dispatchers can adopt new scalar fields and compatible method signatures, but availability, lifecycle, argument marshalling and domain-specific behavior still require consumer tests.

Paths#

A target is a class reachable from a Solver: a service ("forcing", "surface2d.groundwater"), an element kind ("node", "link"), a sub-view of one ("node.stats", "link.xsect") or a standalone class ("xsect", "output"). A member path is a target plus a member name: "node.depth", "link.stats.max_filling", "forcing.node_lat_inflow".

from openswmm.engine import Solver, catalog

entry = catalog.lookup("link.stats.max_filling")
entry["type"], entry["access"], entry["units"]      # ('float', 'r', 'fraction')

[m["name"] for m in catalog.members("node.stats")]  # max_depth, max_overflow, ...

call = catalog.lookup("forcing.node_lat_inflow")
[(p["name"], p["required"]) for p in call["params"]]
# [('node', True), ('value', True), ('mode', False), ('persist', False)]

with Solver("model.inp", "model.rpt", "model.out") as solver:
    stats = catalog.resolve(solver, "node.stats", "J1")   # the live NodeStatsView
    catalog.lookup("node.depth")["bulk"]                  # 'nodes.depths'

Functions#

load()

The whole catalog as a dict (targets, members, functions, records, enums and the C exports).

targets()

Every target by name, with its class, module and, for element kinds, the collection that holds them.

members(target)

The members of one target (or of all targets).

lookup(path)

One member by path; raises KeyError naming the path.

resolve(solver, target, key)

The live object for a target on an open solver; key is an element ID or index for element targets.

unit_label(kind, unit_system, flow_units)

The label for a unit kind in a model’s units.

Units#

Ordinary 1D fields use model units; fixed-unit fields, including many 2D quantities and temperature values, retain the units documented by their APIs. The catalog records a unit kind per float field: a system-dependent kind (length, volume, flow, velocity, …), a fixed one (fraction, percent, dimensionless, temperature, …) or a literal unit (m/s). unit_label turns a kind into the label for a model:

with Solver("model.inp", "model.rpt", "model.out") as solver:
    kind = catalog.lookup("node.depth")["units"]                      # 'length'
    catalog.unit_label(kind, solver.unit_system, solver.flow_units)   # 'ft'
    catalog.unit_label("flow", solver.unit_system, solver.flow_units) # 'CFS'

UNIT_KINDS is the set of symbolic kinds that have a label.

Keeping the catalog current#

python/scripts/gen_catalog.py generates catalog.json from the type stubs, the Cython sources and the C header documentation. Units, exclusions and member corrections the sources cannot express live in python/scripts/catalog_overrides.json. After changing a stub, a .pyx file or a header, regenerate it:

python python/scripts/gen_catalog.py          # rewrite catalog.json
python python/scripts/gen_catalog.py --check  # what CI runs

--check fails when the committed catalog is stale, when a public class is neither reachable nor excluded, or when a float field has no unit kind (mark a true ratio dimensionless). python/tests/test_catalog.py adds the runtime contract: every catalogued member exists on its compiled class, every public attribute is catalogued or excluded, and every target resolves on an opened model.

Consumer compatibility#

  • Gymnasium add_field consumes scalar numeric element properties; its per-cell collector supports selected 2D service methods. Writable metadata does not guarantee that a field can be actuated during a running episode.

  • MCP dispatches JSON-compatible catalog operations. Session-owned lifecycle methods and Python callbacks are deliberately restricted; new standalone classes require constructor handling.

  • WASM generates a raw C API from its pinned engine submodule, plus numeric enums from the Python source. This is independent of the Python catalog. Its current 1D build excludes 2D, mesh groundwater and surface quality; its TypeScript convenience classes cover only part of the Python object API.

After adding native exports or changing a stub, rebuild the compiled extensions, regenerate the catalog, and run both engine and consumer contract tests. An unchanged package version does not prove that two development builds expose the same catalog or native symbols. A missing newly added extension (for example _transport) requires a complete rebuild/reinstall, not an import fallback.