Skip to content

Commit

Permalink
New model builder (#301)
Browse files Browse the repository at this point in the history
* address ambiguous template naming in inline_dependencies

* fill non-graphs, propagate the optional arg requirement

* update tests

* adjusting tests for inlining

* update 223p templates

* update 223

* add model builder impl

* add QUDT

* update templates

* allow adding triples directly to the builder graph

* allow Library to find template definitions from dependent libraries

* fixing tests to catch warnings, test dependency tracking

* use non-deprecated warnings; make sure warnings are emitted when necessary

* clarify when dependencies are infered for SHACL shapes -> templates

* update dependencies

* adding documentation on templates

* placeholder for further template docs

* clarify load order

* update jsonschema, skip mypy checking on imports

* fix API response and test to handle Brick

* updating 223p and templates

* filtering validationcontext by severity

* add shacl_validate/infer functions and use these as the entrypoint. Augment tests to check both shacl engines

* fix interactions with shacl inference

* tightening up the implementation and use of the shacl_* methods

* support specifying shacl engine in the API

* update tests; test both pyshacl and topquadrant

* add brick-tq-shacl dep

* add TODOs

* Formatting

* no more 3.8!

* ignoring some imported packages without type annotations

* more type annotations

* add types, ignore type errors for imports

* update mypy, fix some issues and ignore some others

* fix union type annotation

* update docker containers

* 3.8.1 python for higher

* add back python 3.8

* change 3.8 version

* add test for finding reasons with a given severity

* update brick-tq-shacl, fix type signature

* remove debug serializations

* bump shacl version

* fixing skolemization for validation

* update 223p, fix merge error

* fix notebook

* fix the merge

* remove qudt from commit

* remove more bad merge

* fix more bad merge

* remove more bad merge

* add s223 namespace, parameter prop on template builder

* add model builder draft

* add call syntax to simplify evaluating templates

* do not waste cycles re-binding namespaces on copied graphs

* update model builder notebook

* add java for topquadrant support

* use topquadrant in 223 notebook

* updating dockerfile; this started failing, possibly because of updated ubuntu

* properly initialize graph superclass
  • Loading branch information
gtfierro authored May 29, 2024
1 parent 88f7daf commit f1e5890
Show file tree
Hide file tree
Showing 13 changed files with 77,407 additions and 2,467 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v4 # for topquadrant shacl support
with:
distribution: 'temurin'
java-version: '21'
- name: setup-python
uses: actions/setup-python@v5
with:
Expand Down
143 changes: 143 additions & 0 deletions buildingmotif/model_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import secrets
from typing import Dict, List, Optional, Union

from rdflib import BNode, Graph, Literal, Namespace, URIRef
from rdflib.store import Store
from rdflib.term import Node

from buildingmotif.dataclasses import Library, Template
from buildingmotif.namespaces import RDF, RDFS


class TemplateBuilderContext(Graph):
"""
A context for building templates. This class allows the user to
add templates to the context and then access them by name. The
context also allows the user to compile all of the templates in
the context into a single graph.
"""

def __init__(self, ns: Namespace, store: Optional[Union[Store, str]] = None):
"""
Creates a new TemplateBuilderContext. The context will create
entities in the given namespace.
:param ns: The namespace to use for the context
:param store: An optional backing store for the graph; ok to leave blank unless
you are experiencing performance issues using TemplateBuilderContext
"""
self.templates: Dict[str, Template] = {}
self.wrappers: List[TemplateWrapper] = []
self.ns: Namespace = ns
super(TemplateBuilderContext, self).__init__(
store=store or "default", identifier=None
)

def add_template(self, template: Template):
"""
Adds a template to the context with all of its dependencies
inlined. Allows the user of the context to access the template
by name.
:param template: The template to add to the context
"""
self.templates[template.name] = template.inline_dependencies()

def add_templates_from_library(self, library: Library):
"""
Adds all of the templates from a library to the context
:param library: The library to add to the context
"""
for template in library.get_templates():
self.add_template(template)

def __getitem__(self, template_name):
if template_name in self.templates:
w = TemplateWrapper(self.templates[template_name], self.ns)
self.wrappers.append(w)
return w
else:
raise KeyError(f"Invalid template name: {template_name}")

def compile(self) -> Graph:
"""
Compiles all of the template wrappers and concatenates them into a single Graph
:return: A graph containing all of the compiled templates
"""
graph = Graph()
graph += self
for wrapper in self.wrappers:
graph += wrapper.compile()
# add a label to every instance if it doesn't have one. Make
# the label the same as the value part of the URI
for s, o in graph.subject_objects(predicate=RDF.type):
if (s, RDFS.label, None) not in graph:
# get the 'value' part of the o URI using qname
_, _, value = graph.namespace_manager.compute_qname(str(o))
graph.add((s, RDFS.label, Literal(value)))
return graph


class TemplateWrapper:
def __init__(self, template: Template, ns: Namespace):
"""
Creates a new TemplateWrapper. The wrapper is used to bind
parameters to a template and then compile the template into
a graph.
:param template: The template to wrap
:param ns: The namespace to use for the wrapper; all bindings will be added to this namespace
"""
self.template = template
self.bindings: Dict[str, Node] = {}
self.ns = ns

def __call__(self, **kwargs):
for k, v in kwargs.items():
self[k] = v
return self

def __getitem__(self, param):
if param in self.bindings:
return self.bindings[param]
elif param not in self.template.all_parameters:
raise KeyError(f"Invalid parameter: {param}")
# if the param is not bound, then invent a name
# by prepending the parameter name to a random string
self.bindings[param] = self.ns[param + "_" + secrets.token_hex(4)]
return self.bindings[param]

def __setitem__(self, param, value):
if param not in self.template.all_parameters:
raise KeyError(f"Invalid parameter: {param}")
# if value is not a URIRef, Literal or BNode, then put it in the namespace
if not isinstance(value, (URIRef, Literal, BNode)):
value = self.ns[value]
# check datatype of value is URIRef, Literal or BNode
if not isinstance(value, (URIRef, Literal, BNode)):
raise TypeError(f"Invalid type for value: {type(value)}")
self.bindings[param] = value

@property
def parameters(self):
return self.template.parameters

def compile(self) -> Graph:
"""
Compiles the template into a graph. If there are still parameters
to be bound, then the template will be returned. Otherwise, the
template will be filled and the resulting graph will be returned.
:return: A graph containing the compiled template
:rtype: Graph
"""
tmp = self.template.evaluate(self.bindings)
# if this is true, there are still parameters to be bound
if isinstance(tmp, Template):
bindings, graph = tmp.fill(self.ns, include_optional=False)
self.bindings.update(bindings)
return graph
else:
return tmp
2 changes: 2 additions & 0 deletions buildingmotif/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
QUDTDV = Namespace("http://qudt.org/vocab/dimensionvector/")
UNIT = Namespace("http://qudt.org/vocab/unit/")

# ASHRAE namespaces
BACNET = Namespace("http://data.ashrae.org/bacnet/2020#")
S223 = Namespace("http://data.ashrae.org/standard223#")

BM = Namespace("https://nrel.gov/BuildingMOTIF#")
CONSTRAINT = Namespace("https://nrel.gov/BuildingMOTIF/constraints#")
Expand Down
2 changes: 0 additions & 2 deletions buildingmotif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ def copy_graph(g: Graph, preserve_blank_nodes: bool = True) -> Graph:
:rtype: Graph
"""
c = Graph()
for pfx, ns in g.namespaces():
c.bind(pfx, ns)
new_prefix = secrets.token_hex(4)
for t in g.triples((None, None, None)):
assert isinstance(t, tuple)
Expand Down
69 changes: 50 additions & 19 deletions libraries/ashrae/223p/nrel-templates/connections.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,63 @@ duct:
@prefix s223: <http://data.ashrae.org/standard223#> .
P:name a s223:Duct ;
s223:hasMedium s223:Medium-Air ;
s223:connectsAt P:a, P:b .
s223:cnx P:a, P:b .
# issue here is that 'connectsAt' requires a,b to be conn points
# but we can't instantiate that class directly *and* being a conn point
# involves other properties that must be included (e.g. hasmedium).
# TODO: how to solve this?
# P:a a s223:ConnectionPoint .
# P:b a s223:ConnectionPoint .
zone-air-inlet-cp:
junction:
body: >
@prefix P: <urn:___param___#> .
@prefix s223: <http://data.ashrae.org/standard223#> .
P:name a s223:InletZoneConnectionPoint ;
s223:mapsTo P:mapsto ;
s223:hasMedium s223:Medium-Air .
P:mapsto a s223:InletConnectionPoint ;
s223:hasMedium s223:Medium-Air .
optional: ["mapsto"]

zone-air-outlet-cp:
body: >
@prefix P: <urn:___param___#> .
@prefix s223: <http://data.ashrae.org/standard223#> .
P:name a s223:OutletZoneConnectionPoint ;
s223:mapsTo P:mapsto ;
s223:hasMedium s223:Medium-Air .
P:mapsto a s223:OutletConnectionPoint ;
s223:hasMedium s223:Medium-Air .
optional: ["mapsto"]
P:name a s223:Junction ;
s223:hasMedium s223:Medium-Air ;
s223:cnx P:in1, P:in2, P:out1, P:out2, P:out3, P:out4, P:out5, P:out6, P:out7,
P:out8, P:out9, P:out10, P:out11, P:out12, P:out13, P:out14, P:out15, P:out16 .
optional: ["in2","in3","in4","in5","out2", "out3", "out4", "out5", "out6", "out7", "out8", "out9","out10","out11","out12","out13","out14","out15", "out16"]
dependencies:
- template: air-inlet-cp
args: {"name": "in1"}
- template: air-inlet-cp
args: {"name": "in2"}
- template: air-inlet-cp
args: {"name": "in3"}
- template: air-inlet-cp
args: {"name": "in4"}
- template: air-inlet-cp
args: {"name": "in5"}
- template: air-outlet-cp
args: {"name": "out1"}
- template: air-outlet-cp
args: {"name": "out2"}
- template: air-outlet-cp
args: {"name": "out3"}
- template: air-outlet-cp
args: {"name": "out4"}
- template: air-outlet-cp
args: {"name": "out5"}
- template: air-outlet-cp
args: {"name": "out6"}
- template: air-outlet-cp
args: {"name": "out7"}
- template: air-outlet-cp
args: {"name": "out8"}
- template: air-outlet-cp
args: {"name": "out9"}
- template: air-outlet-cp
args: {"name": "out10"}
- template: air-outlet-cp
args: {"name": "out11"}
- template: air-outlet-cp
args: {"name": "out12"}
- template: air-outlet-cp
args: {"name": "out13"}
- template: air-outlet-cp
args: {"name": "out14"}
- template: air-outlet-cp
args: {"name": "out15"}
- template: air-outlet-cp
args: {"name": "out16"}
Loading

0 comments on commit f1e5890

Please sign in to comment.