Skip to content

Commit 2a11331

Browse files
committed
support rdfs class in template loading
1 parent a0a2c35 commit 2a11331

File tree

7 files changed

+66
-11
lines changed

7 files changed

+66
-11
lines changed

buildingmotif/dataclasses/library.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ def _load_from_ontology(
209209
) -> "Library":
210210
"""
211211
Load a library from an ontology graph. This proceeds as follows.
212-
First, get all entities in the graph that are instances of *both* owl:Class
213-
and sh:NodeShape. (this is "candidates")
212+
First, get all entities in the graph that are instances of *both* sh:NodeShape
213+
and either owl:Class or rdfs:Class (these are the "candidates").
214214
215215
For each candidate, use the utility function to parse the NodeShape and turn
216216
it into a Template.

buildingmotif/dataclasses/shape_collection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ def infer_templates(self, library: "Library") -> None:
207207
)
208208
continue
209209

210-
class_candidates = set(self.graph.subjects(rdflib.RDF.type, rdflib.OWL.Class))
210+
class_candidates = set(
211+
self.graph.subjects(rdflib.RDF.type, rdflib.OWL.Class)
212+
).union(self.graph.subjects(rdflib.RDF.type, rdflib.RDFS.Class))
211213
shape_candidates = set(
212214
self.graph.subjects(rdflib.RDF.type, rdflib.SH.NodeShape)
213215
)

buildingmotif/template_matcher.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ def defined_in(self, node: Node, graph: Graph) -> bool:
7171
cache = self.in_cache[id(graph)]
7272
# populate cache if necessary
7373
if node not in cache:
74-
cache[node] = (node, RDF.type, OWL.Class) in graph
74+
cache[node] = (node, RDF.type, OWL.Class) in graph or (
75+
node,
76+
RDF.type,
77+
RDFS.Class,
78+
) in graph
7579
return cache[node]
7680

7781

buildingmotif/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,16 @@ def resolve_library(node_uri: URIRef) -> Optional[str]:
263263
break
264264
return library
265265

266+
def is_class(node_uri: URIRef) -> bool:
267+
for graph in [shape_graph, *depedency_graphs.values()]:
268+
if (node_uri, RDF.type, OWL.Class) in graph or (
269+
node_uri,
270+
RDF.type,
271+
RDFS.Class,
272+
) in graph:
273+
return True
274+
return False
275+
266276
def add_dependency(node_uri: URIRef, param: URIRef):
267277
if not isinstance(node_uri, URIRef):
268278
return
@@ -286,9 +296,7 @@ def process_shape(shape_node: Node, focus_param: URIRef):
286296
return
287297
visited.add(key)
288298

289-
if (shape_node, RDF.type, OWL.Class) in shape_graph and isinstance(
290-
shape_node, URIRef
291-
):
299+
if isinstance(shape_node, URIRef) and is_class(shape_node):
292300
body.add((focus_param, RDF.type, shape_node))
293301

294302
for cls in shape_graph.objects(shape_node, SH["class"]):

docs/explanations/shapes-and-templates.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ Evaluating the resulting template will generate a graph that validates against t
2929
When BuildingMOTIF loads a Library, it makes an attempt to find any shapes defined within it.
3030
The way this happens depends on how the library is loaded:
3131
- *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
32-
- *Loading library from ontology file*: loads all instances of `sh:NodeShape` in the provided graphc
32+
- *Loading library from ontology file*: loads all instances of `sh:NodeShape` in the provided graph
3333

3434
```{important}
35-
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".
35+
BuildingMOTIF *only* loads shapes which are instances of `sh:NodeShape` **and** either `owl:Class` or `rdfs:Class`. The assumption is that class-ified shapes could be "instantiated".
3636
```
3737

3838
Each shape is "decompiled" into components from which a Template can be constructed.

docs/explanations/templates.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ BuildingMOTIF can also infer a template from certain SHACL shape definitions.
6565
This happens when a Library is loaded into BuildingMOTIF that contains an RDF graph; this can happen by loading the RDF graph directly (via `Library.load(ontology_graph="path to graph")`)
6666
or by loading in a directory that contains RDF graphs (via `Library.load(directory="directory with .ttl files")`).
6767

68-
Given an RDF graph, BuildingMOTIF will create a template for each instance of `sh:NodeShape` *provided* that it is also an instance of `owl:Class`.
68+
Given an RDF graph, BuildingMOTIF will create a template for each instance of `sh:NodeShape` *provided* that it is also an instance of `owl:Class` or `rdfs:Class`.
6969
In the following RDF graph, BuildingMOTIF would create a tempalte for `vav_shape` but not `sensor_shape`:
7070

7171
```ttl

tests/unit/dataclasses/test_shape_collection.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from buildingmotif.dataclasses.library import Library
1010
from buildingmotif.dataclasses.shape_collection import ShapeCollection
11-
from buildingmotif.namespaces import BMOTIF, BRICK
11+
from buildingmotif.namespaces import BMOTIF, BRICK, PARAM
1212

1313

1414
def test_create_shape_collection(clean_building_motif):
@@ -185,3 +185,44 @@ def test_shape_to_query(clean_building_motif, shape_name, query_clauses):
185185
assert clause in query, query
186186
# Validate that the query executes correctly
187187
g.query(query)
188+
189+
190+
def test_infer_templates_accepts_rdfs_class(clean_building_motif):
191+
graph = Graph()
192+
graph.parse(
193+
data="""
194+
@prefix : <urn:test#> .
195+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
196+
@prefix sh: <http://www.w3.org/ns/shacl#> .
197+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
198+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
199+
200+
:ontology a owl:Ontology .
201+
202+
:MyShape a sh:NodeShape, rdfs:Class ;
203+
sh:targetClass :MyShape ;
204+
sh:property [
205+
sh:path :hasChild ;
206+
sh:minCount 1 ;
207+
sh:name "child" ;
208+
sh:class :ChildClass ;
209+
] .
210+
211+
:ChildShape a sh:NodeShape, rdfs:Class ;
212+
sh:targetClass :ChildClass ;
213+
sh:property [
214+
sh:path :label ;
215+
sh:minCount 1 ;
216+
sh:datatype xsd:string ;
217+
] .
218+
219+
:ChildClass a rdfs:Class .
220+
""",
221+
format="turtle",
222+
)
223+
224+
lib = Library.load(
225+
ontology_graph=graph, infer_templates=True, run_shacl_inference=False
226+
)
227+
template = lib.get_template_by_name("urn:test#MyShape")
228+
assert (PARAM["name"], RDF.type, URIRef("urn:test#MyShape")) in template.body

0 commit comments

Comments
 (0)