Managing Metadata in Graphs

Graphs are the primary unit of management for Brick models. brickschema provides two ways of managing graphs and the triples inside them:

  • as a single bag of triples (Graph, a subclass of rdflib.Graph)
  • as a union of individually addressable bags of triples (GraphCollection, a subclass of rdflib.Dataset)

Both Graph and GraphCollection posess the ability to import triples from a variety of sources — online resources, local files, etc — and perform reasoning/inference, validation and querying over those triples. However, Graph does not provide any addressable subdivision of the ingested triples; once those triples are loaded into the graph, they are all considered part of the same flat set.

GraphCollection introduces a new method, load_graph(), which imports triples from the provided source into its own graph. This graph is an instance of Graph and can be queried, reasoned, validated just like other graphs. The name of the graph is a URI given by any owl:Ontology definition inside the graph (which can be overridden). The encapsulating GraphCollection object can query the constituent graphs individually, or as a union.

The advantage of GraphCollection over Graph is that it makes it easier to upgrade individual graphs — ontology definitions, building instances, etc — separately.

from brickschema import GraphCollection
from brickschema.namespaces import BRICK
from rdflib import Namespace

 # Create a new graph collection
 gc = GraphCollection(load_brick=True)
 # the Brick ontology is loaded under its own URI
 # the other graph is the "default" graph which contains
 # reasoned and inferred triples
 assert URIRef(BRICK) in g.graph_names
 assert len(g.graph_names) == 2

 brick = gc.graph(URIRef(BRICK))
 # we can work with the Brick ontology graph independently
 equipment_classes = brick.query("""
     SELECT ?equipment_class
     WHERE { ?equipment_class rdf:type brick:EquipmentClass }""")

 # add a building graph with a sensor
 EX1 = Namespace("urn:ex1#")
 # referring to the graph implicitly creates it
 bldg = gc.graph(EX1)
 bldg.add((EX1["a"], A, BRICK["Sensor"]))

 # perform SHACL reasoning on the graph; reasoned triples
 # will be added to the default graph
 gc.expand("shacl")

 # now we can query the graph collection all together
 assert len(g.graph_names) == 3
 assert URIRef(EX1) in g.graph_names
 assert URIRef(BRICK) in g.graph_names
 res = gc.query("SELECT * WHERE { ?x a brick:Sensor }")
 assert len(res) == 1, "Should have 1 sensor from adding graph"

 # now we can replace the Brick ontology definition with a newer version
 gc.remove_graph(URIRef(BRICK))
 gc.load_graph("https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl", graph_name=BRICK)

Second Example

from brickschema.graph import GraphCollection
from brickschema.namespaces import BRICK, A
from rdflib import URIRef, Namespace

# in-memory graph
g = GraphCollection()

# load Brick ontology
g.load_graph("https://sparql.gtf.fyi/ttl/Brick1.3rc1.ttl", format="turtle")

# declare namespace for the entities in the "instance" model
BLDG = Namespace("urn:building-instance/")

# grab the graph for the building instance model so we can add triples to it
bldg_graph = g.graph(URIRef(BLDG))

# now we can add triples to the building
bldg_graph.add((BLDG["my-building"], A, BRICK.Building))
bldg_graph.add((BLDG["my-sensor"], A, BRICK.Zone_Air_Temperature_Sensor))

# when we run queries, run them against the "collection"
res = g.query("""SELECT * WHERE {
    ?sensor rdf:type/rdfs:subClassOf* brick:Temperature_Sensor
}""")
assert len(res) == 1

# we can save the building graph separately
bldg_graph = g.graph(URIRef(BLDG))
bldg_graph.serialize("my-building.ttl", format="turtle")