===============================
Programmatic model construction
===============================

.. note::

   **Engine:** OpenSWMM 6 — refactored.  Documents
   :class:`openswmm.engine.ModelBuilder`.

.. currentmodule:: openswmm.engine

The :class:`ModelBuilder` constructs a complete SWMM model in Python,
without needing a ``.inp`` file.  Use it for:

* Generating models from a database or GeoJSON.
* Synthetic models for testing and benchmarking.
* Optimisation loops where the network topology is the decision
  variable.
* Templates: build a base model in code, then ``write()`` the
  resulting ``.inp`` file.

Reference: ``openswmm_model.h``.

----

Class signature
===============

.. code-block:: python

    class ModelBuilder:
        def __init__(self) -> None: ...

The builder is **standalone** — it does not need a Solver.  Once
populated and validated, call :meth:`to_solver` to obtain a Solver
ready to run, or :meth:`write(path)` to dump an ``.inp`` file.

----

Key methods
===========

Adding objects
--------------

.. list-table::
   :header-rows: 1
   :widths: 40 60

   * - Method
     - Returns
   * - :meth:`add_node(id, type)`
     - Append a node with :class:`NodeType` ``type``.
   * - :meth:`add_link(id, type)`
     - Append a link with :class:`LinkType` ``type``.
   * - :meth:`add_subcatchment(id)` / :meth:`add_subcatch(id)`
     - Append a subcatchment.
   * - :meth:`add_gage(id)`
     - Append a rain gage.
   * - :meth:`pop_last_node(id)` / :meth:`pop_last_link(id)`
     - Pop the most recently added node / link (by id sanity check).

Setting node properties
-----------------------

.. list-table::
   :header-rows: 1
   :widths: 40 60

   * - Method
     - Action
   * - :meth:`set_node_invert(idx, elev)`
     - Invert elevation.
   * - :meth:`set_node_max_depth(idx, depth)`
     - Maximum depth above invert.

Setting link properties
-----------------------

.. list-table::
   :header-rows: 1
   :widths: 40 60

   * - Method
     - Action
   * - :meth:`set_link_nodes(idx, from_node, to_node)`
     - Connect link ``idx`` between two node indices.
   * - :meth:`set_link_length(idx, length)`
     - Conduit length.
   * - :meth:`set_link_roughness(idx, n)`
     - Manning's *n*.
   * - :meth:`set_link_xsect(idx, shape, geom1, geom2, geom3, geom4)`
     - Cross-section: :class:`XSectShape` and up to four geometry parameters.

Finalising
----------

.. list-table::
   :header-rows: 1
   :widths: 35 65

   * - Method
     - Action
   * - :meth:`validate()`
     - Check the model for connectivity / consistency.  Raises on issues.
   * - :meth:`finalize()`
     - Lock the model — no more topology changes after this.
   * - :meth:`write(path)`
     - Write the assembled model to a SWMM ``.inp`` file.
   * - :meth:`handle()`
     - Opaque handle (mostly for plugin authors).
   * - :meth:`to_solver()`
     - Convert the builder into a runnable :class:`Solver`.

----

End-to-end example
==================

.. code-block:: python

    from openswmm.engine import (
        ModelBuilder, NodeType, LinkType, XSectShape, EngineState,
    )

    m = ModelBuilder()

    # nodes
    j1   = m.add_node("J1",   NodeType.JUNCTION)
    out1 = m.add_node("OUT1", NodeType.OUTFALL)
    m.set_node_invert(j1,   100.0)
    m.set_node_invert(out1, 99.0)
    m.set_node_max_depth(j1, 5.0)

    # link
    c1 = m.add_link("C1", LinkType.CONDUIT)
    m.set_link_nodes(c1, j1, out1)
    m.set_link_length(c1, 300.0)
    m.set_link_roughness(c1, 0.013)
    m.set_link_xsect(c1, XSectShape.CIRCULAR, 1.0, 0.0, 0.0, 0.0)

    # validate and run
    m.validate()
    m.finalize()
    m.write("synthetic.inp")               # optional: dump for inspection

    solver = m.to_solver()
    solver.start()
    while solver.state == EngineState.RUNNING:
        if solver.step() != 0:
            break
    solver.end()
    solver.report()
    solver.close()
    solver.destroy()

The :meth:`to_solver` Solver behaves identically to one constructed
from a parsed ``.inp`` — every domain class, forcing, control rule,
and output reader works.

----

Common recipes
==============

Build a simple grid topology
----------------------------

.. code-block:: python

    m = ModelBuilder()

    # 4×4 grid of junctions, single outfall in the corner
    grid = {}
    for r in range(4):
        for c in range(4):
            nid = f"J_{r}_{c}"
            grid[(r, c)] = m.add_node(nid, NodeType.JUNCTION)
            m.set_node_invert(grid[(r, c)], 100.0 - 0.5 * (r + c))
            m.set_node_max_depth(grid[(r, c)], 5.0)
    out = m.add_node("OUT", NodeType.OUTFALL)
    m.set_node_invert(out, 90.0)

    # Conduits along every row and column
    cid = 0
    for r in range(4):
        for c in range(3):
            link = m.add_link(f"R_{r}_{c}", LinkType.CONDUIT)
            m.set_link_nodes(link, grid[(r, c)], grid[(r, c + 1)])
            m.set_link_length(link, 100.0)
            m.set_link_roughness(link, 0.013)
            m.set_link_xsect(link, XSectShape.CIRCULAR, 0.6, 0, 0, 0)
            cid += 1

    # Bottom-right corner connects to the outfall
    final = m.add_link("FINAL", LinkType.CONDUIT)
    m.set_link_nodes(final, grid[(3, 3)], out)
    m.set_link_length(final, 50.0)
    m.set_link_roughness(final, 0.013)
    m.set_link_xsect(final, XSectShape.CIRCULAR, 1.5, 0, 0, 0)

    m.validate()
    m.finalize()

Pop a misconfigured node
------------------------

If you've appended an object and immediately realise it's wrong, pop
it before it's referenced anywhere:

.. code-block:: python

    bad = m.add_node("typo_id", NodeType.JUNCTION)
    m.pop_last_node("typo_id")           # rolls back the last add

Build, dump, then handoff to a colleague
----------------------------------------

.. code-block:: python

    m = ModelBuilder()
    # ... build ...
    m.validate()
    m.finalize()
    m.write("scenario_a.inp")
    # Send scenario_a.inp anywhere — runs in any SWMM-compatible engine.

----

EngineState requirements & exceptions
=====================================

.. list-table::
   :header-rows: 1
   :widths: 30 25 45

   * - Method group
     - Required state
     - Notes
   * - ``add_*``
     - pre-``validate``
     - Order matters: links reference nodes by index.
   * - ``set_*``
     - pre-``finalize``
     - After ``finalize`` topology is locked.
   * - ``validate``
     - any
     - Raises :class:`EngineError` with a descriptive message on
       inconsistency (e.g. dangling node, missing xsect).
   * - ``write`` / ``finalize`` / ``to_solver``
     - after ``validate``
     - n/a

----

See also
========

* :doc:`editing` — modify an existing parsed model in place.
* :doc:`solver` — run the model returned by :meth:`to_solver`.
* :doc:`nodes`, :doc:`links`, :doc:`subcatchments` — query / mutate
  state once the Solver is open.
