Model a treatment train#

This tutorial models a conventional surface-water treatment train: coagulation, flocculation, sedimentation, filtration, and disinfection (CDC overview). You will connect the stages, mark the system boundary, explore the result, save it, and load it again.

Treatment train with sludge and filter backwash branches

The model records equipment, connectivity, and treatment intent. It does not calculate removal, dose, or contact time.

1. Import the stages#

plumb.watr.drinking_water provides one class per stage. Each subclasses a WaTr equipment class and fixes its process, objective, and named ports.

Stage

Class

WaTr parent

Coagulation

CoagulationStage

CoagulationBasin

Flocculation

FlocculationStage

FlocculationBasin

Clarification

ClarificationStage

SedimentationTank

Filtration

RapidSandFiltrationStage

RapidSandFilter

Disinfection

ChlorinationStage

ChlorinationUnit

from plumb import Model, connect
from plumb.watr import SedimentationTank
from plumb.watr.drinking_water import (
    ChlorinationStage,
    ClarificationStage,
    CoagulationStage,
    FlocculationStage,
    RapidSandFiltrationStage,
)

2. Connect the stages#

Connect named ports along the production path. The clarifier’s sludge outlet and the filter’s backwash ports are separate points, so waste cannot end up feeding the next stage.

with Model("urn:example/treatment#") as model:
    with model.system("treatment-system") as system:
        coagulation = CoagulationStage("coagulation")
        flocculation = FlocculationStage("flocculation")
        clarifier = ClarificationStage("clarifier")
        sand_filter = RapidSandFiltrationStage("sand-filter")
        disinfection = ChlorinationStage("chlorination-contact")

        connect(coagulation.outlet, flocculation.inlet, name="coagulated-water")
        connect(flocculation.outlet, clarifier.inlet, name="flocculated-water")
        connect(
            clarifier.clarified_water, sand_filter.inlet, name="clarified-water"
        )
        connect(
            sand_filter.filtered_water, disinfection.inlet, name="filtered-water"
        )

        system.boundary(
            coagulation.inlet,
            disinfection.outlet,
            clarifier.sludge,
            sand_filter.backwash_supply,
            sand_filter.backwash_waste,
        )

model.system() groups the stages. boundary() marks the points where flow crosses into or out of the system: raw water in, finished water out, sludge out, and backwash water in and out.

3. Explore the result#

compile() resolves the connections, validates the model, and returns a snapshot:

resolved = model.compile()

Look up the clarifier and list its process and objective:

resolved_clarifier = resolved["clarifier"]

for process in resolved_clarifier.has_process:
    print("process:", process.meta.label)

for objective in resolved_clarifier.has_treatment_objective:
    print("objective:", objective.meta.label)
process: Settling
objective: Clarification

Look up a pipe by its name and see which points it joins:

pipe = resolved["clarified-water"]
print(pipe.type, pipe.medium)
for point in pipe.connects_at:
    print(point.owner.name, point.label)
Pipe <enum Fluid-Water>
clarifier clarified_water
sand-filter inlet

List the points where flow crosses the system boundary:

plant = resolved["treatment-system"]
for point in plant.has_boundary_connection_point:
    print(point.owner.name, point.label, point.type, point.medium)
chlorination-contact outlet OutletConnectionPoint <enum Fluid-Water>
clarifier sludge OutletConnectionPoint <enum Fluid-Sludge>
coagulation inlet InletConnectionPoint <enum Fluid-Water>
sand-filter backwash_supply InletConnectionPoint <enum Fluid-Water>
sand-filter backwash_waste OutletConnectionPoint <enum Fluid-Water>

Sludge is its own medium. Both filter outlets carry water and are told apart by name.

Follow the flow instead of reading pipes one at a time. find_all_next() steps over pipes and points and returns the equipment downstream, nearest first:

resolved["coagulation"].find_all_next()
[<FlocculationStage 'flocculation'>,
 <ClarificationStage 'clarifier'>,
 <RapidSandFiltrationStage 'sand-filter'>,
 <ChlorinationStage 'chlorination-contact'>]

Class arguments match subclasses, so asking for a SedimentationTank upstream of disinfection finds the clarifier:

resolved["chlorination-contact"].find_previous(SedimentationTank)
<ClarificationStage 'clarifier'>

4. Save and load#

model.save("treatment.ttl")

The file imports the drinking-water ontology, so from_graph() selects the right package. Query by the WaTr parent class:

loaded = Model.from_graph("treatment.ttl")
loaded.find_all(SedimentationTank)
[<ClarificationStage 'clarifier'>]

The clarifier was saved as a ClarificationStage, which is a kind of SedimentationTank. A loaded model has its pipes and points already, so it can be walked without resolving:

loaded["clarifier"].find_next()
<RapidSandFiltrationStage 'sand-filter'>

5. Draw it#

from plumb import flowsheet

flowsheet(loaded, title="Conventional treatment train")
../_images/aae88da50caff5812c4f0849943215609932321cc153355a10c3602d5d5484ed.svg

Unconnected boundary points are drawn as feed and product terminals.

Next#

Instrument a plant adds pumps, residuals handling, an influent property set, sensors, and actuators to this train. The script version is examples/watr/treatment/watr_quickstart.py.