Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into update-223p
Browse files Browse the repository at this point in the history
  • Loading branch information
gtfierro committed Apr 16, 2024
2 parents eb3d28b + 59fa3a4 commit 845d2d1
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 13 deletions.
13 changes: 12 additions & 1 deletion buildingmotif/building_motif/building_motif.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from contextlib import contextmanager
from typing import Optional

from rdflib import Graph
from rdflib.namespace import NamespaceManager
Expand All @@ -24,16 +25,26 @@
class BuildingMOTIF(metaclass=Singleton):
"""Manages BuildingMOTIF data classes."""

def __init__(self, db_uri: str, log_level=logging.WARNING) -> None:
def __init__(
self,
db_uri: str,
shacl_engine: Optional[str] = "pyshacl",
log_level=logging.WARNING,
) -> None:
"""Class constructor.
:param db_uri: database URI
:type db_uri: str
:param shacl_engine: the name of the engine to use for validation: "pyshacl" or "topquadrant". Using topquadrant
requires Java to be installed on this machine, and the "topquadrant" feature on BuildingMOTIF,
defaults to "pyshacl"
:type shacl_engine: str, optional
:param log_level: logging level of detail
:type log_level: int
:default log_level: INFO
"""
self.db_uri = db_uri
self.shacl_engine = shacl_engine
self.engine = create_engine(
db_uri,
echo=False,
Expand Down
8 changes: 6 additions & 2 deletions buildingmotif/dataclasses/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ def validate(
data_graph = copy_graph(self.graph)

# validate the data graph
valid, report_g, report_str = shacl_validate(data_graph, shapeg, engine=engine)
valid, report_g, report_str = shacl_validate(
data_graph, shapeg, engine=self._bm.shacl_engine
)
return ValidationContext(
shape_collections,
shapeg,
Expand Down Expand Up @@ -231,7 +233,9 @@ def compile(

model_graph = copy_graph(self.graph).skolemize()

return shacl_inference(model_graph, ontology_graph, engine)
return shacl_inference(
model_graph, ontology_graph, engine=self._bm.shacl_engine
)

def test_model_against_shapes(
self,
Expand Down
137 changes: 137 additions & 0 deletions buildingmotif/progressive_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from collections import Counter
from secrets import token_hex
from typing import Callable, Dict, List

from rdflib import Graph
from rdflib.term import Node

from buildingmotif.dataclasses import Library, Template
from buildingmotif.namespaces import PARAM
from buildingmotif.template_matcher import (
_ontology_lookup_cache,
get_semantic_feasibility,
)
from buildingmotif.utils import Triple

# from rdflib.compare import graph_diff


def _is_parameterized_triple(triple: Triple) -> bool:
"""
Returns true if a parameter appears in the triple
"""
st = (str(triple[0]), str(triple[1]), str(triple[2]))
return st[0].startswith(PARAM) or st[1].startswith(PARAM) or st[2].startswith(PARAM)


def _template_from_triples(lib: Library, triples: List[Triple]) -> Template:
g = Graph()
for t in triples:
g.add(t)
return lib.create_template(token_hex(4), g)


def compatible_triples(
a: Triple, b: Triple, sf_func: Callable[[Node, Node], bool]
) -> bool:
"""
Two triples are compatible if:
- they are the same
- they are pairwise semantically feasible
"""
if a == b:
return True
for (a_term, b_term) in zip(a, b):
if not sf_func(a_term, b_term):
return False
return True


def progressive_plan(templates: List[Template], context: Graph) -> List[Template]:
"""
We are given a list of templates; this is either a set of templates that
the model author supplies directly or are derived from sets of shapes that
need to be fulfilled.
We want to optimize the population of these templates: there may be
redundant information between them, or there may be some unique/rare parts
of templates whose populations can be postponed.
"""
# present result as a sequence of (parameterized) triples?

# greedy algorithm:
# start with the most common (substitutable) triple amongst all templates
# then choose the next most common triple, and so on.
# Yield triples that have parameters; automatically include
# non-parameterized triples
templates = [template.inline_dependencies() for template in templates]
histogram: Counter = Counter()

inv: Dict[Triple, Template] = {}

for templ in templates:
cache = _ontology_lookup_cache()
for body_triple in templ.body.triples((None, None, None)):
found = False
for hist_triple in histogram.keys():
sf_func = get_semantic_feasibility(
templ.body, inv[hist_triple].body, context, cache
)
if compatible_triples(body_triple, hist_triple, sf_func):
print(body_triple)
print(hist_triple)
print("-" * 50)
found = True
histogram[hist_triple] += 1
break
if not found:
histogram[body_triple] = 1
inv[body_triple] = templ

# Start with the most common triple. We want to generate a sequence of triples
# that maximizes the number of templates that are included in the resulting graph.
# This is analogous to creating a left-biased CDF of (original) templates included

# idea 1: just iterate through most common histogram
# This has no guarantee that the sequence is optimal *or* connected.
# We probably want to prioritize creating a connected sequence..
most_common = histogram.most_common()
triples = [triple[0] for triple in most_common]

template_sequence: List[Template] = []

lib = Library.create("temporary")
buffer: List[Triple] = []
for triple in triples:
buffer.append(triple)
if _is_parameterized_triple(triple):
template_sequence.append(_template_from_triples(lib, buffer))
buffer.clear()
if len(buffer) > 0:
template_sequence.append(_template_from_triples(lib, buffer))

# TODO: need to mark when a new template is satisfied

# can look at the CDF for a site specification as part of paper evaluation

# stub of an alternative approach...
# for pair in combinations(templates, 2):
# for pair in permutations(templates, 2):
# t1, t2 = pair[0].in_memory_copy(), pair[1].in_memory_copy()
# print(t1.body.serialize())
# print("-" * 100)
# print(t2.body.serialize())
# print("-" * 100)
# tm = TemplateMatcher(t1.body, t2, context)
# for mapping, subgraph in tm.building_mapping_subgraphs_iter(
# size=tm.largest_mapping_size
# ):
# pprint(mapping)
# print(subgraph.serialize())

# # both, first, second = graph_diff(pair[0].body, pair[1].body)
# # print(both.serialize())
# print("*" * 100)
# print("*" * 100)

return template_sequence
4 changes: 3 additions & 1 deletion buildingmotif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ def shacl_validate(


def shacl_inference(
data_graph: Graph, shape_graph: Optional[Graph] = None, engine="topquadrant"
data_graph: Graph,
shape_graph: Optional[Graph] = None,
engine: Optional[str] = "topquadrant",
) -> Graph:
"""
Infer new triples in the data graph using the shape graph.
Expand Down
13 changes: 9 additions & 4 deletions tests/unit/api/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def test_create_model_bad_name(client, building_motif):


def test_validate_model(client, building_motif, shacl_engine):
building_motif.shacl_engine = shacl_engine
# Set up
brick = Library.load(ontology_graph="tests/unit/fixtures/Brick.ttl")
assert brick is not None
Expand Down Expand Up @@ -312,7 +313,8 @@ def test_validate_model(client, building_motif, shacl_engine):
assert results.get_json()["reasons"] == {}


def test_validate_model_bad_model_id(client, building_motif):
def test_validate_model_bad_model_id(client, building_motif, shacl_engine):
building_motif.shacl_engine = shacl_engine
# Set up
library = Library.load(ontology_graph="tests/unit/fixtures/shapes/shape1.ttl")
assert library is not None
Expand All @@ -328,7 +330,8 @@ def test_validate_model_bad_model_id(client, building_motif):
assert results.status_code == 404


def test_validate_model_no_args(client, building_motif):
def test_validate_model_no_args(client, building_motif, shacl_engine):
building_motif.shacl_engine = shacl_engine
# Set up
BLDG = Namespace("urn:building/")
model = Model.create(name=BLDG)
Expand All @@ -347,7 +350,8 @@ def test_validate_model_no_args(client, building_motif):
assert results.get_json()["reasons"] == {}


def test_validate_model_no_library_ids(client, building_motif):
def test_validate_model_no_library_ids(client, building_motif, shacl_engine):
building_motif.shacl_engine = shacl_engine
# Set up
BLDG = Namespace("urn:building/")
model = Model.create(name=BLDG)
Expand Down Expand Up @@ -416,7 +420,8 @@ def test_validate_model_bad_args(client, building_motif):
assert results.status_code == 400


def test_test_model_against_shapes(client, building_motif):
def test_test_model_against_shapes(client, building_motif, shacl_engine):
building_motif.shacl_engine = shacl_engine
# Load libraries
Library.load(ontology_graph=str(PROJECT_DIR / "libraries/brick/Brick-subset.ttl"))
ashrae_g36 = Library.load(
Expand Down
13 changes: 10 additions & 3 deletions tests/unit/dataclasses/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def test_update_model_manifest(clean_building_motif):


def test_validate_model_manifest(clean_building_motif, shacl_engine):
clean_building_motif.shacl_engine = shacl_engine
m = Model.create(name="https://example.com", description="a very good model")
m.graph.add((URIRef("https://example.com/vav1"), A, BRICK.VAV))

Expand Down Expand Up @@ -91,6 +92,7 @@ def test_validate_model_manifest(clean_building_motif, shacl_engine):


def test_validate_model_manifest_with_imports(clean_building_motif, shacl_engine):
clean_building_motif.shacl_engine = shacl_engine
m = Model.create(name="https://example.com", description="a very good model")
m.graph.add((URIRef("https://example.com/vav1"), A, BRICK.VAV))

Expand Down Expand Up @@ -125,11 +127,12 @@ def test_validate_model_manifest_with_imports(clean_building_motif, shacl_engine
)

# validate against manifest -- should pass now
result = m.validate(engine=shacl_engine)
result = m.validate()
assert result.valid, result.report_string


def test_validate_model_explicit_shapes(clean_building_motif, shacl_engine):
clean_building_motif.shacl_engine = shacl_engine
# load library
Library.load(ontology_graph="tests/unit/fixtures/Brick1.3rc1-equip-only.ttl")
lib = Library.load(ontology_graph="tests/unit/fixtures/shapes/shape1.ttl")
Expand Down Expand Up @@ -158,6 +161,7 @@ def test_validate_model_with_failure(bm: BuildingMOTIF, shacl_engine):
"""
Test that a model correctly validates
"""
bm.shacl_engine = shacl_engine
shape_graph_data = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
Expand Down Expand Up @@ -202,6 +206,7 @@ def test_validate_model_with_failure(bm: BuildingMOTIF, shacl_engine):

def test_model_compile(bm: BuildingMOTIF, shacl_engine):
"""Test that model compilation gives expected results"""
bm.shacl_engine = shacl_engine
small_office_model = Model.create("http://example.org/building/")
small_office_model.graph.parse(
"tests/unit/fixtures/smallOffice_brick.ttl", format="ttl"
Expand Down Expand Up @@ -236,6 +241,7 @@ def test_get_manifest(clean_building_motif):


def test_validate_with_manifest(clean_building_motif, shacl_engine):
clean_building_motif.shacl_engine = shacl_engine
g = Graph()
g.parse(
data="""
Expand Down Expand Up @@ -272,12 +278,13 @@ def test_validate_with_manifest(clean_building_motif, shacl_engine):
manifest = model.get_manifest()
manifest.add_graph(manifest_g)

ctx = model.validate(None, engine=shacl_engine)
ctx = model.validate()
assert not ctx.valid, "Model validated but it should throw an error"


def test_get_validation_severity(clean_building_motif, shacl_engine):
NS = Namespace("urn:ex/")
clean_building_motif.shacl_engine = shacl_engine
g = Graph()
g.parse(
data="""
Expand Down Expand Up @@ -337,7 +344,7 @@ def test_get_validation_severity(clean_building_motif, shacl_engine):
manifest = model.get_manifest()
manifest.add_graph(manifest_g)

ctx = model.validate(None, engine=shacl_engine)
ctx = model.validate()
assert not ctx.valid, "Model validated but it should throw an error"

# check that only valid severity values are accepted
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/fixtures/template-inline-test/1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
parent:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:Equipment ;
brick:hasPoint P:sensor ;
brick:hasPart P:dep .
dependencies:
- template: child
args: {"name": "dep", "child-opt-arg": "sensor"}

child:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:Equipment ;
brick:hasPoint P:child-opt-arg .
optional: ["child-opt-arg"]
17 changes: 17 additions & 0 deletions tests/unit/fixtures/templates-variadic/1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
vav:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:VAV ;
brick:hasPoint P:part\+ ;
brick:hasPoint P:point\* .
dependencies:
- template: temp-sensor
args: {"name": "point"}

temp-sensor:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:Temperature_Sensor ;
brick:hasUnit P:unit\? .
Loading

0 comments on commit 845d2d1

Please sign in to comment.