Save, load, and edit models#
Save a model as RDF, load RDF back as the same typed objects, edit them, and save again. Loading needs to know which generated packages, or registries, describe the classes in the file; this page covers how they are chosen and how to pass your own.
Save#
from plumb import Model, connect
from plumb.watr import Pump, Tank
from plumb.watr.drinking_water import ClarificationStage, CoagulationStage
with Model("urn:example:plant#") as authored:
intake = Pump("intake-pump", label="Intake pump")
coagulation = CoagulationStage("coagulation")
clarifier = ClarificationStage("clarifier")
holding = Tank("sludge-holding")
connect(intake, coagulation.inlet, name="raw-water")
connect(coagulation.outlet, clarifier.inlet, name="coagulated-water")
connect(clarifier.sludge, holding, name="sludge-line")
authored.save("plant.ttl")
save() resolves the model and writes the snapshot. The format follows the
extension: .ttl, .nt, .xml or .rdf, or .jsonld; format=
overrides it. graph() returns the same RDF as an rdflib graph. Neither
validates; see Validate a model.
The file declares an owl:Ontology that imports each ontology it uses, and
records the installed version of each one that declares a version:
from rdflib import OWL
graph = authored.graph()
for _, _, dependency in graph.triples((None, OWL.imports, None)):
print(dependency, graph.value(dependency, OWL.versionInfo))
https://watermetadata.org/ontology/watr 0.3.rc1
https://watermetadata.org/plumb/drinking-water# None
Loading reads these imports to choose packages.
Set ontology metadata#
The ontology’s IRI defaults to the namespace without the trailing # or
/. Set it, a label, extra imports, and prefixes on the constructor:
model = Model(
"urn:example:plant#",
name="Example plant",
ontology_iri="urn:example:plant-model",
imports=["urn:example:project-vocabulary"],
prefixes={"qudt": "http://qudt.org/schema/qudt/"},
)
Load#
Model.from_graph() takes a path, URL, or rdflib graph:
model = Model.from_graph("plant.ttl")
model.by_class()
{plumb.watr.drinking_water.ClarificationStage: [<ClarificationStage 'clarifier'>],
plumb.watr.OutletConnectionPoint: [<OutletConnectionPoint 'clarifier-clarified_water'>,
<OutletConnectionPoint 'clarifier-sludge'>,
<OutletConnectionPoint 'coagulation-outlet'>,
<OutletConnectionPoint 'intake-pump-out'>],
plumb.watr.InletConnectionPoint: [<InletConnectionPoint 'clarifier-inlet'>,
<InletConnectionPoint 'coagulation-inlet'>,
<InletConnectionPoint 'intake-pump-in'>,
<InletConnectionPoint 'sludge-holding-in'>],
plumb.watr.Pipe: [<Pipe 'coagulated-water'>,
<Pipe 'raw-water'>,
<Pipe 'sludge-line'>],
plumb.watr.drinking_water.CoagulationStage: [<CoagulationStage 'coagulation'>],
plumb.watr.Pump: [<Pump 'intake-pump'>],
plumb.watr.Tank: [<Tank 'sludge-holding'>]}
Every resource with a generated class becomes an object of the most
specific class. Points and pipes load as ordinary objects, not as
connect() calls. Named ports come back, so model["clarifier"].sludge
works as it did when authored. Instance IRIs keep their namespace, which is
inferred from the resources unless you pass namespace=.
How packages are chosen#
Each installed package registers its classes in a registry. from_graph()
matches the graph’s owl:imports against the installed packages:
Package |
Imports it matches |
|---|---|
|
the ontology’s IRI |
|
the extension’s IRI |
A domain package’s registry includes its base classes, so when a graph imports both WaTr and drinking water, the drinking-water registry is used and covers both.
The imported version must match the installed package, so a file saved by a different ontology release is not read with the wrong classes:
from rdflib import Graph, Literal, URIRef
old = Graph().parse("plant.ttl")
watr = URIRef("https://watermetadata.org/ontology/watr")
old.set((watr, OWL.versionInfo, Literal("0.1")))
try:
Model.from_graph(old)
except ValueError as error:
print(error)
WATR ontology version in graph is 0.1, but the installed package has 0.3.rc1
Pass registries#
RDF without imports, or using a package you compiled
yourself, needs registries=:
bare = Graph().parse("plant.ttl")
bare.remove((None, OWL.imports, None))
try:
Model.from_graph(bare)
except ValueError as error:
print(error)
no installed ontology package matches this graph's owl:imports; pass registries= explicitly
from plumb.watr import drinking_water
model = Model.from_graph(bare, registries=drinking_water.REGISTRY)
type(model["clarifier"]).__name__
'ClarificationStage'
Pass a list for several, such as [watr.REGISTRY, s223.hvac.REGISTRY]. Every
generated package exposes REGISTRY, and a compiled package’s registry
includes the classes it extends.
Unknown classes and malformed data#
Resources whose classes no registry knows stay in the graph but are not loaded as objects. A generated attribute that points at one raises, because the object would be incomplete:
from rdflib import RDF, Namespace
EX = Namespace("urn:example:plant#")
S223 = Namespace("http://data.ashrae.org/standard223#")
extended = Graph().parse("plant.ttl")
extended.add((EX.mystery, RDF.type, URIRef("urn:other#Gadget")))
extended.add((EX.clarifier, S223.contains, EX.mystery))
try:
Model.from_graph(extended)
except Exception as error:
print(type(error).__name__, error)
ModelingError cannot load <urn:example:plant#mystery> as contains_: expected a resource with a generated Python type (include its registry)
The fix is usually the missing registry. strict=False skips such values
instead, along with resources whose types conflict, several values for a
single-valued attribute, and named ports that cannot be identified. Skipped
triples stay in the graph.
model = Model.from_graph(extended, strict=False)
"mystery" in [entity.name for entity in model], (EX.mystery, None, None) in model.graph()
(False, True)
Load with inference#
infer=True runs the packages’ SHACL-AF rules before loading, so facts the
rules derive become attributes. Pass shapes= for other rules:
model = Model.from_graph("plant.ttl", infer=True)
Inference is off by default because it adds triples to the graph.
Edit and save#
A loaded model is backed by its graph, and edits write through to it:
from plumb.watr import enums
model = Model.from_graph("plant.ttl")
clarifier = model["clarifier"]
clarifier.label = "Clarifier 1"
clarifier.has_role.add(enums.values.Role_Primary)
with model:
Tank("sludge-tank")
model.save("plant-updated.ttl")
Only the triples behind generated attributes change; everything else in the
graph stays. A graph passed to from_graph() is updated in place. Edits
made through rdflib after loading are not seen by the objects.
A loaded model has no resolution step, so connect() and model.system()
do not work on it. Edit the concrete pipes and points instead:
try:
connect(model["intake-pump"], model["sludge-tank"])
except Exception as error:
print(type(error).__name__, error)
ModelingError deferred S223/WaTr connections cannot yet be added to a graph-backed model; edit its concrete Connection and ConnectionPoint objects instead
Search what you loaded#
Search, containment, and flow walks work on a loaded model directly:
model["intake-pump"].find_all_next()
[<CoagulationStage 'coagulation'>,
<ClarificationStage 'clarifier'>,
<Tank 'sludge-holding'>]