Use a domain package#

A domain package adds reusable equipment and property schemas on top of WaTr. This tutorial builds two aerobic reactor stages with plumb.watr.asm2d, the activated-sludge package.

1. Create the stages#

AerobicStage fixes its processes, objectives, roles, and named inlet and outlet ports:

from plumb import Model, connect
from plumb.qudt import QuantityAmount as Q, units
from plumb.watr.asm2d import AerobicStage, AerobicDesign, OxygenOutletState

print(AerobicStage.describe_ports())
AerobicStage port requirements
Unconditional ports:
  - inlet: direction="in" -> InletConnectionPoint (count 1..1); medium roots: Fluid-Water
  - outlet: direction="out" -> OutletConnectionPoint (count 1..1); medium roots: Fluid-Water
  - direction="in" -> InletConnectionPoint (count 1..any); medium roots: Mix-Fluid
  - direction="out" -> OutletConnectionPoint (count 1..any); medium roots: Mix-Fluid

Create two stages in a system:

with Model("urn:example:two-stages#") as model:
    with model.system("treatment-system") as system:
        first = AerobicStage("first")
        second = AerobicStage("second")

2. Apply design values#

AerobicDesign is a property schema compiled from SHACL. It requires an oxygen transfer coefficient and defaults the volume to 1333 m³:

print(AerobicDesign.describe())
Aerobic basin volume in m³ (default 1333) and oxygen transfer coefficient KLa in h⁻¹.
volume: Volume [Cubic Meter; http://qudt.org/vocab/unit/M3] default=1333.0 — https://watermetadata.org/plumb/asm2d#AerobicVolumeShape
  aspects=Nominal; property_class=QuantifiableProperty
oxygen_transfer_coefficient: frequency [Reciprocal Hour; http://qudt.org/vocab/unit/PER-HR] required — https://watermetadata.org/plumb/asm2d#OxygenTransferCoefficientShape
  aspects=Nominal; property_class=QuantifiableProperty

Create one design and apply it to both stages. Each stage gets its own properties:

design = AerobicDesign(oxygen_transfer_coefficient=Q(10.0, units.PER_HR))
first.apply_properties(design)
second.apply_properties(design)

model["first-volume"], model["second-volume"]
(<QuantifiableProperty 'first-volume'>, <QuantifiableProperty 'second-volume'>)

The volume was not given, so it takes the default:

design.volume
1333.0

Set the oxygen concentration leaving each stage:

outlet_oxygen = OxygenOutletState(oxygen_concentration=Q(2.0, units.GM_PER_M3))
first_oxygen = first.outlet.apply_properties(outlet_oxygen)
second_oxygen = second.outlet.apply_properties(outlet_oxygen)

dict(first_oxygen)
{'oxygen_concentration': <QuantifiableProperty 'first-outlet-oxygen-concentration'>}

A bare number uses the field’s default unit: OxygenOutletState(oxygen_concentration=2.0) means 2 g/m³. An explicit Q(...) keeps its unit.

3. Connect the stages#

Mark where flow enters and leaves the system, then connect the first stage to the second:

system.boundary(first.inlet, second.outlet)
link = connect(first.outlet, second.inlet, name="interstage")

4. Resolve and validate#

resolved = model.resolve()
connection = resolved.connection(link)
connection.name, connection.medium
('interstage', <enum Fluid-Water>)
model.validate().ok
True

Validation uses the shapes bundled with the package, the same ones the Python classes were compiled from.

5. Load it back#

The graph imports the ASM2d ontology, so from_graph() selects the package. Loading keeps the values as written; it does not fill in defaults.

graph = resolved.graph()
loaded = Model.from_graph(graph)
loaded["first"]
<AerobicStage 'first'>
loaded["first-volume"].has_value
1333.0

Next#