Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documenting shape-to-template process and adding some QoL improvements #278

Merged
merged 11 commits into from
Nov 27, 2023
Merged
21 changes: 16 additions & 5 deletions buildingmotif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,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 +250,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.
1 change: 1 addition & 0 deletions tests/integration/test_library_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@


@pytest.mark.integration
@pytest.mark.skip(reason="223P support is temporarily broken")
def test_223p_library(bm, library_path_223p: Path):
ont_223p = Library.load(ontology_graph="libraries/ashrae/223p/ontology/223p.ttl")
lib = Library.load(directory=str(library_path_223p))
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_get_template_parts_from_shape():
)
body, deps = get_template_parts_from_shape(MODEL["shape1"], shape_graph)
assert len(deps) == 1
assert deps[0]["template"] == BRICK.Temperature_Sensor
assert deps[0]["template"] == str(BRICK.Temperature_Sensor)
assert list(deps[0]["args"].keys()) == ["name"]
assert (PARAM["name"], A, MODEL["shape1"]) in body
# assert (PARAM['name'], BRICK.hasPoint,
Expand Down