Connect equipment#
connect(a, b) records that flow goes from a to b. You do not create
pipes or connection points. When the model resolves, the solver picks the
points, the medium, and the connection class. See
How connections are resolved.
Read a class’s port requirements#
describe_ports() lists the connection points a class requires: direction,
count, and allowed media.
from plumb import Model, connect
from plumb.watr import GravityThickener, Pump, enums
from plumb.watr.drinking_water import ClarificationStage, RapidSandFiltrationStage
values = enums.values
print(Pump.describe_ports())
Pump port requirements
Unconditional ports:
- direction="in" -> InletConnectionPoint (count 1..any); medium roots: Constituent-Refrigerant, Mix-Fluid
- direction="out" -> OutletConnectionPoint (count 1..any); medium roots: Constituent-Refrigerant, Mix-Fluid
"in" is flow arriving at the equipment, "out" is flow leaving it, and
"bi" is either. A medium root admits everything beneath it. Domain classes
also list their named points:
print(ClarificationStage.describe_ports())
ClarificationStage port requirements
Unconditional ports:
- clarified_water: direction="out" -> OutletConnectionPoint (count 1..1); medium roots: Fluid-Water
- inlet: direction="in" -> InletConnectionPoint (count 1..1); medium roots: Fluid-Water
- sludge: direction="out" -> OutletConnectionPoint (count 1..1); medium roots: Fluid-Sludge
- direction="out" -> OutletConnectionPoint (count 2..any); medium roots: Mix-Fluid
The same information is structured on meta.port_contract:
for slot in Pump.meta.port_contract.slots:
print(slot.direction, slot.point_class.__name__, slot.min_count, slot.max_count)
in InletConnectionPoint 1 None
out OutletConnectionPoint 1 None
Connect and check#
model = Model("urn:example:residuals#")
with model:
clarifier = ClarificationStage("clarifier")
sand_filter = RapidSandFiltrationStage("sand-filter")
sludge_pump = Pump("sludge-pump")
thickener = GravityThickener("thickener")
sludge = connect(clarifier, sludge_pump, name="sludge-draw")
report = model.check()
print(report.status)
for issue in report.issues:
print(issue.message)
underconstrained
connection or named point choices remain ambiguous: sludge-draw; fix a point direction or medium, or connect it to equipment that determines the choice
check() solves without building anything. It returns one of:
complete: exactly one answer.underconstrained: more than one. Here the clarifier has a water and a sludge outlet, and a WaTr pump accepts either.unsatisfiable: none. The issues name the authored facts in conflict.
An underconstrained report is truthy; test report.complete. resolve(),
graph(), save(), and validate() raise AmbiguousModelError or
UnsatisfiableModelError instead of guessing.
Add a hint#
Add the narrowest fact you know. Here, the medium:
sludge.medium = values.Fluid_Sludge
connect(sludge_pump, thickener, name="sludge-to-thickener")
print(model.check().status)
complete
The second connection needs no hint: the pump now carries sludge.
Hint |
Example |
|---|---|
Medium |
|
Named point |
|
Declared port |
|
Connection class |
|
A named point selects that point outright:
connect(clarifier.clarified_water, sand_filter.inlet, name="clarified-water")
print(model.check().status)
complete
.port() declares a point the shapes do not mention, with a stable name.
Its medium= overrides the class’s medium domain.
Tank(..., medium=[...]) records s223:hasMedium on the tank but does not
constrain its connections. Put the medium on the port or connection instead.
Inspect the result#
resolved = model.resolve()
resolved.explain(sludge)
sludge-draw: clarifier-sludge -> sludge-pump-in
medium: Fluid-Sludge (explicit connection)
- selected OutletConnectionPoint clarifier-sludge
- selected InletConnectionPoint sludge-pump-in
- sludge-draw.medium = Fluid-Sludge
The explanation gives the chosen points, the medium, and the reason. The same
values are on .medium, .medium_reason, and .evidence.
resolved.connection(handle) returns the concrete connection:
pipe = resolved.connection(sludge)
pipe, pipe.medium, list(pipe.connects_at)
(<Pipe 'sludge-draw'>,
<enum Fluid-Sludge>,
[<OutletConnectionPoint 'clarifier-sludge'>,
<InletConnectionPoint 'sludge-pump-in'>])
Derived points and pipes exist only in the snapshot. Before resolution,
equipment.ports holds points that already exist and declared ports. After,
the snapshot’s copy holds every concrete point:
list(sludge_pump.ports)
[]
list(resolved["sludge-pump"].ports)
[<InletConnectionPoint 'sludge-pump-in'>,
<OutletConnectionPoint 'sludge-pump-out'>]
The snapshot can be walked along its pipes. through= follows only pipes
carrying one medium, here the sludge line:
start = resolved["clarifier"]
start.find_all_next(), start.find_all_next(through={"medium": values.Fluid_Sludge})
([<RapidSandFiltrationStage 'sand-filter'>,
<Pump 'sludge-pump'>,
<GravityThickener 'thickener'>],
[<Pump 'sludge-pump'>, <GravityThickener 'thickener'>])
See Find objects in a model for the other search and walk methods.
Any edit to the model invalidates the snapshot.
Describe a connection#
Set properties on the handle. They are copied to the resolved connection:
sludge.label = "Clarifier sludge draw"
spent = connect(sand_filter.backwash_waste, recovery_tank, name="spent-backwash")
spent.has_role.add(values.Role_Backwash)
Roles do not affect which medium the solver picks.
Add more points#
Shape requirements are minimums. Request an extra point with ports="new":
connect(source, mixer, ports="new")
Pair two ports that carry the same flow through a device. Pairing makes their
media equal and records s223:pairedConnectionPoint:
feed_in = exchanger.port("feed-in", direction="in")
feed_out = exchanger.port("feed-out", direction="out")
feed_in.pair(feed_out)
Next#
Group equipment into systems and spaces collects connected equipment into systems with boundary points.
Attach quantities describes what flows in a connection.