Skip to content

Commit

Permalink
Merge branch 'develop' into Add-Application-explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
haneslinger authored Nov 30, 2023
2 parents dffc872 + 7ce4036 commit a31ddff
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 24 deletions.
42 changes: 24 additions & 18 deletions buildingmotif/dataclasses/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,24 +260,8 @@ def _load_from_ontology(

lib = cls.create(ontology_name, overwrite=overwrite)

class_candidates = set(ontology.subjects(rdflib.RDF.type, rdflib.OWL.Class))
shape_candidates = set(ontology.subjects(rdflib.RDF.type, rdflib.SH.NodeShape))
candidates = class_candidates.intersection(shape_candidates)

# stores the lookup from template *names* to template *ids*
# this is necessary because while we know the *name* of the dependee templates
# for each dependent template, we don't know the *id* of the dependee templates,
# which is necessary to populate the dependencies
template_id_lookup: Dict[str, int] = {}
dependency_cache: Dict[int, List[Dict[Any, Any]]] = {}
for candidate in candidates:
assert isinstance(candidate, rdflib.URIRef)
partial_body, deps = get_template_parts_from_shape(candidate, ontology)
templ = lib.create_template(str(candidate), partial_body)
dependency_cache[templ.id] = deps
template_id_lookup[str(candidate)] = templ.id

lib._resolve_template_dependencies(template_id_lookup, dependency_cache)
# infer shapes from any class/nodeshape candidates in the graph
lib._infer_shapes_from_graph(ontology)

# load the ontology graph as a shape_collection
shape_col_id = lib.get_shape_collection().id
Expand All @@ -287,6 +271,26 @@ def _load_from_ontology(

return lib

def _infer_shapes_from_graph(self, graph: rdflib.Graph):
"""Infer shapes from a graph and add them to this library.
:param graph: graph to infer shapes from
:type graph: rdflib.Graph
"""
class_candidates = set(graph.subjects(rdflib.RDF.type, rdflib.OWL.Class))
shape_candidates = set(graph.subjects(rdflib.RDF.type, rdflib.SH.NodeShape))
candidates = class_candidates.intersection(shape_candidates)
template_id_lookup: Dict[str, int] = {}
dependency_cache: Dict[int, List[Dict[Any, Any]]] = {}
for candidate in candidates:
assert isinstance(candidate, rdflib.URIRef)
partial_body, deps = get_template_parts_from_shape(candidate, graph)
templ = self.create_template(str(candidate), partial_body)
dependency_cache[templ.id] = deps
template_id_lookup[str(candidate)] = templ.id

self._resolve_template_dependencies(template_id_lookup, dependency_cache)

def _load_shapes_from_directory(self, directory: pathlib.Path):
"""Helper method to read all graphs in the given directory into this
library.
Expand All @@ -305,6 +309,8 @@ def _load_shapes_from_directory(self, directory: pathlib.Path):
f"Could not parse file {filename}: {e}"
)
raise e
# infer shapes from any class/nodeshape candidates in the graph
self._infer_shapes_from_graph(shape_col.graph)

@classmethod
def _load_from_directory(
Expand Down
23 changes: 18 additions & 5 deletions buildingmotif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def get_template_parts_from_shape(
pshapes = shape_graph.objects(subject=shape_name, predicate=SH["property"])
for pshape in pshapes:
property_path = shape_graph.value(pshape, SH["path"])
if property_path is None:
raise Exception(f"no sh:path detected on {shape_name}")
# TODO: expand otypes to include sh:in, sh:or, or no datatype at all!
otypes = list(
shape_graph.objects(
Expand All @@ -232,11 +234,16 @@ def get_template_parts_from_shape(
(path, otype, mincount) = property_path, otypes[0], mincounts[0]
assert isinstance(mincount, Literal)

for _ in range(int(mincount)):
param = _gensym()
param_name = shape_graph.value(pshape, SH["name"])

for num in range(int(mincount)):
if param_name is not None:
param = PARAM[f"{param_name}{num}"]
else:
param = _gensym()
body.add((root_param, path, param))
deps.append({"template": otype, "args": {"name": param}})
# body.add((param, RDF.type, otype))
deps.append({"template": str(otype), "args": {"name": param}})
body.add((param, RDF.type, otype))

if (shape_name, RDF.type, OWL.Class) in shape_graph:
body.add((root_param, RDF.type, shape_name))
Expand All @@ -245,9 +252,15 @@ def get_template_parts_from_shape(
for cls in classes:
body.add((root_param, RDF.type, cls))

classes = shape_graph.objects(shape_name, SH["targetClass"])
for cls in classes:
body.add((root_param, RDF.type, cls))

nodes = shape_graph.objects(shape_name, SH["node"])
for node in nodes:
deps.append({"template": node, "args": {"name": "name"}}) # tie to root param
deps.append(
{"template": str(node), "args": {"name": "name"}}
) # tie to root param

return body, deps

Expand Down
1 change: 1 addition & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ parts:
- caption: Explainations
chapters:
- file: explanations/ingresses.md
- file: explanations/shapes-and-templates.md
- caption: Appendix
chapters:
- file: bibliography.md
182 changes: 182 additions & 0 deletions docs/explanations/shapes-and-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
jupytext:
cell_metadata_filter: -all
formats: md:myst
text_representation:
extension: .md
format_name: myst
kernelspec:
display_name: Python 3
language: python
name: python3
---

# Shapes and Templates

Shapes and Templates interact in interesting ways in BuildingMOTIF.
In this document, we explain the utility and function of these interactions.

Recall that a **Shape** (SHACL shape) is a set of conditions and constraints over RDF graphs, and
a **Template** is a function that generates an RDF graph.

## Converting Shapes to Templates

BuildingMOTIF automatically converts shapes to templates.
Evaluating the resulting template will generate a graph that validates against the shape.

When BuildingMOTIF loads a Library, it makes an attempt to find any shapes defined within it.
The way this happens depends on how the library is loaded:
- *Loading library from directory or git repository*: BuildingMOTIF searches for any RDF files in the directory (recursively) and loads them into a Shape Collection; loads any instances of `sh:NodeShape` in the union of these RDF files
- *Loading library from ontology file*: loads all instances of `sh:NodeShape` in the provided graphc

```{important}
BuildingMOTIF *only* loads shapes which are instances of *both* `sh:NodeShape` **and** `owl:Class`. The assumption is that `owl:Class`-ified shapes could be "instantiated".
```

Each shape is "decompiled" into components from which a Template can be constructed.
The implementation of this decompilation is in the [`get_template_parts_from_shape`](/reference/apidoc/_autosummary/buildingmotif.utils.html#buildingmotif.utils.get_template_parts_from_shape) method.
BuildingMOTIF currently recognizes the following SHACL properties:
- `sh:property`
- `sh:qualifiedValueShape`
- `sh:node`
- `sh:class`
- `sh:targetClass`
- `sh:datatype`
- `sh:minCount` / `sh:qualifiedMinCount`
- `sh:maxCount` / `sh:qualifiedMaxCount`

BuildingMOTIF currently uses the name of the SHACL shape as the name of the generated Template.
All other parameters (i.e., nodes corresponding to `sh:property`) are given invented names *unless*
there is a `sh:name` attribute on the property shape.

### Example

Consider the following shape which has been loaded into BuildingMOTIF as part of a Library:

```ttl
# myshapes.ttl
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <urn:example/> .
: a owl:Ontology .
:vav a sh:NodeShape, owl:Class ;
sh:targetClass brick:Terminal_Unit ;
sh:property [
sh:path brick:hasPart ;
sh:qualifiedValueShape [ sh:node :heating-coil ] ;
sh:name "hc" ;
sh:qualifiedMinCount 1 ;
] ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Supply_Air_Flow_Sensor ] ;
sh:qualifiedMinCount 1 ;
] ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Supply_Air_Temperature_Sensor ] ;
sh:name "sat" ;
sh:qualifiedMinCount 1 ;
] ;
.
:heating-coil a sh:NodeShape, owl:Class ;
sh:targetClass brick:Heating_Coil ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Position_Command ] ;
sh:name "damper_pos" ; # will be used as the parameter name
sh:qualifiedMinCount 1 ;
] ;
.
```

<details>

This code creates `myshapes.ttl` for you in the current directory.

```{code-cell} python3
with open("myshapes.ttl", "w") as f:
f.write("""
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <urn:example/> .
: a owl:Ontology .
:vav a sh:NodeShape, owl:Class ;
sh:targetClass brick:Terminal_Unit ;
sh:property [
sh:path brick:hasPart ;
sh:qualifiedValueShape [ sh:node :heating-coil ] ;
sh:name "hc" ;
sh:qualifiedMinCount 1 ;
] ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Supply_Air_Flow_Sensor ] ;
sh:qualifiedMinCount 1 ;
] ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Supply_Air_Temperature_Sensor ] ;
sh:name "sat" ;
sh:qualifiedMinCount 1 ;
] ;
.
:heating-coil a sh:NodeShape, owl:Class ;
sh:targetClass brick:Heating_Coil ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Position_Command ] ;
sh:name "damper_pos" ; # will be used as the parameter name
sh:qualifiedMinCount 1 ;
] ;
.
""")
```

</details>

If this was in a file `myshapes.ttl`, we would load it into BuildingMOTIF as follows:

```{code-cell} python3
from buildingmotif import BuildingMOTIF
from buildingmotif.dataclasses import Library
# in-memory instance
bm = BuildingMOTIF("sqlite://")
# load library
brick = Library.load(ontology_graph="https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl")
lib = Library.load(ontology_graph="myshapes.ttl")
```

Once the library has been loaded, all of the shapes have been turned into templates.
We can load the template by name (using its *full URI* from the shape) as if it were
defined explicitly:

```{code-cell} python3
# reading the template out by name
template = lib.get_template_by_name("urn:example/vav")
# dump the body of the template
print(template.body.serialize())
```

As with other templates, we often want to *inline* all dependencies to get a sense of what metadata will be added to the graph.

```{code-cell} python3
# reading the template out by name
template = lib.get_template_by_name("urn:example/vav").inline_dependencies()
# dump the body of the template
print(template.body.serialize())
```

Observe that the generated template uses the `sh:name` property of each property shape to inform the paramter name. If this is not provided (e.g. for the `brick:Supply_Air_Flow_Sensor` property shape), then a generated parameter will be used.
11 changes: 11 additions & 0 deletions tests/unit/dataclasses/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ def test_load_library_from_ontology(bm: BuildingMOTIF):
assert len(shapeg.graph) > 1


def test_load_library_from_ontology_with_error(bm: BuildingMOTIF):
with pytest.raises(Exception):
Library.load(ontology_graph="tests/unit/fixtures/bad_shape_template.ttl")


def test_load_shapes_with_directory_library(bm: BuildingMOTIF):
lib = Library.load(directory="tests/unit/fixtures/library-shape-test")
assert lib is not None
assert len(lib.get_templates()) == 2


def test_load_library_from_directory(bm: BuildingMOTIF):
lib = Library.load(directory="tests/unit/fixtures/templates")
assert lib is not None
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/fixtures/bad_shape_template.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix : <urn:shape1/> .

: a owl:Ontology .

:vav_shape a owl:Class, sh:NodeShape ;
sh:targetClass brick:VAV ;
sh:property [
# missing sh:path!
sh:qualifiedValueShape [ sh:class brick:Air_Flow_Sensor ] ;
sh:qualifiedMinCount 1 ;
sh:minCount 1;
] ;
.
29 changes: 29 additions & 0 deletions tests/unit/fixtures/libary-shape-test/shape.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix : <urn:shape/> .

: a owl:Ontology .

:vav_shape a owl:Class, sh:NodeShape ;
sh:targetClass brick:VAV ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Air_Flow_Sensor ] ;
sh:qualifiedMinCount 1 ;
sh:minCount 1;
] ;
.

:tu_shape a owl:Class, sh:NodeShape ;
sh:targetClass brick:Terminal_Unit ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Temperature_Sensor ] ;
sh:qualifiedMinCount 1 ;
sh:minCount 1;
] ;
.

26 changes: 26 additions & 0 deletions tests/unit/fixtures/library-shape-test/shape.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@prefix brick: <https://brickschema.org/schema/Brick#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix : <urn:shape/> .

: a owl:Ontology .

:vav_shape a owl:Class, sh:NodeShape ;
sh:targetClass brick:VAV ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Air_Flow_Sensor ] ;
sh:qualifiedMinCount 1 ;
] ;
.

:tu_shape a owl:Class, sh:NodeShape ;
sh:targetClass brick:Terminal_Unit ;
sh:property [
sh:path brick:hasPoint ;
sh:qualifiedValueShape [ sh:class brick:Temperature_Sensor ] ;
sh:qualifiedMinCount 1 ;
] ;
.
Loading

0 comments on commit a31ddff

Please sign in to comment.