Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion buildingmotif/api/views/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,8 @@ def validate_model(models_id: int) -> flask.Response:
return {
"message": vaildation_context.report_string,
"valid": vaildation_context.valid,
"reasons": [x.reason() for x in vaildation_context.diffset],
"reasons": {
focus_node: [gd.reason() for gd in grahdiffs]
for focus_node, grahdiffs in vaildation_context.diffset.items()
},
}, status.HTTP_200_OK
45 changes: 20 additions & 25 deletions buildingmotif/dataclasses/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class ValidationContext:
model: "Model"

@cached_property
def diffset(self) -> Set[GraphDiff]:
def diffset(self) -> Dict[Optional[URIRef], Set[GraphDiff]]:
"""The unordered set of GraphDiffs produced from interpreting the input
SHACL validation report.
"""
Expand All @@ -277,7 +277,7 @@ def as_templates(self) -> List["Template"]:
"""
return diffset_to_templates(self.diffset)

def _report_to_diffset(self) -> Set[GraphDiff]:
def _report_to_diffset(self) -> Dict[Optional[URIRef], Set[GraphDiff]]:
"""Interpret a SHACL validation report and say what is missing.

:return: a set of GraphDiffs that each abstract a SHACL shape violation
Expand All @@ -289,7 +289,7 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
# proppath = SH["property"] | (SH.qualifiedValueShape / SH["property"]) # type: ignore

g = self.report + self._context
diffs: Set[GraphDiff] = set()
diffs: Dict[Optional[URIRef], Set[GraphDiff]] = defaultdict(set)
for result in g.objects(predicate=SH.result):
# check if the failure is due to our count constraint component
focus = g.value(result, SH.focusNode)
Expand All @@ -308,13 +308,9 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
# here, our 'self.focus' is the graph itself, which we don't want to have bound
# to the templates during evaluation (for this specific kind of diff).
# For this reason we override focus to be None
diffs.add(
diffs[None].add(
GraphClassCardinality(
None,
validation_report,
g,
of_class,
int(expected_count),
None, validation_report, g, of_class, int(expected_count)
)
)
elif (
Expand All @@ -323,7 +319,9 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
):
requiring_shape = g.value(result, SH.sourceShape)
expected_class = g.value(requiring_shape, SH["class"])
diffs.add(RequiredClass(focus, validation_report, g, expected_class))
diffs[focus].add(
RequiredClass(focus, validation_report, g, expected_class)
)
elif (
g.value(result, SH.sourceConstraintComponent)
== SH.NodeConstraintComponent
Expand Down Expand Up @@ -352,7 +350,7 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
# extra = g.value(result, SH.sourceShape / proppath) # type: ignore

if focus and (min_count or max_count) and classname:
diffs.add(
diffs[focus].add(
PathClassCount(
focus,
validation_report,
Expand All @@ -367,7 +365,7 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
shapename = g.value(result, SH.sourceShape / shapepath) # type: ignore
if focus and (min_count or max_count) and shapename:
extra_body, deps = get_template_parts_from_shape(shapename, g)
diffs.add(
diffs[focus].add(
PathShapeCount(
focus,
validation_report,
Expand All @@ -382,7 +380,7 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
)
continue
if focus and (min_count or max_count):
diffs.add(
diffs[focus].add(
RequiredPath(
focus,
validation_report,
Expand All @@ -395,7 +393,9 @@ def _report_to_diffset(self) -> Set[GraphDiff]:
return diffs


def diffset_to_templates(diffset: Set[GraphDiff]) -> List["Template"]:
def diffset_to_templates(
grouped_diffset: Dict[Optional[URIRef], Set[GraphDiff]]
) -> List["Template"]:
"""Combine GraphDiff by focus node to generate a list of templates that
reconcile what is "wrong" with the Graph with respect to the GraphDiffs.

Expand All @@ -407,21 +407,16 @@ def diffset_to_templates(diffset: Set[GraphDiff]) -> List["Template"]:
"""
from buildingmotif.dataclasses import Library, Template

related: Dict[URIRef, Set[GraphDiff]] = defaultdict(set)
unfocused: List[Template] = []
lib = Library.create(f"resolve_{token_hex(4)}")
# compute the GROUP BY GraphDiff.focus
for diff in diffset:
if diff.focus is None:
unfocused.extend(diff.resolve(lib))
else:
related[diff.focus].add(diff)

templates = []
# add all templates that don't have a focus node
templates.extend(unfocused)
# now merge all tempaltes together for each focus node
for focus, diffset in related.items():
for focus, diffset in grouped_diffset.items():
if focus is None:
for diff in diffset:
templates.extend(diff.resolve(lib))
continue

templ_lists = (diff.resolve(lib) for diff in diffset)
templs = list(filter(None, chain.from_iterable(templ_lists)))
if len(templs) <= 1:
Expand Down
47 changes: 28 additions & 19 deletions notebooks/Existing-model-validation-example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,23 @@
"output_type": "stream",
"text": [
"Model is valid: False\n",
"Reasons why:\n",
" -http://example.org/building/5-Zone-PVAV-2 needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n",
" -http://example.org/building/5-Zone-PVAV needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n",
" -http://example.org/building/5-Zone-PVAV-1 needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n"
"http://example.org/building/5-Zone-PVAV\n",
" - http://example.org/building/5-Zone-PVAV needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n",
"http://example.org/building/5-Zone-PVAV-1\n",
" - http://example.org/building/5-Zone-PVAV-1 needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n",
"http://example.org/building/5-Zone-PVAV-2\n",
" - http://example.org/building/5-Zone-PVAV-2 needs between 1 and None instances of https://brickschema.org/schema/Brick#Supply_Air_Temperature_Setpoint on path https://brickschema.org/schema/Brick#hasPoint\n"
]
}
],
"source": [
"print(f\"Model is valid: {validation_context.valid}\")\n",
"\n",
"if not validation_context.valid:\n",
" print(\"Reasons why:\")\n",
" for diff in validation_context.diffset:\n",
" print(\" -\" + diff.reason())"
" for focus_node, diffs in validation_context.diffset.items():\n",
" print(focus_node)\n",
" for diff in diffs:\n",
" print(\" - \" + diff.reason())"
]
},
{
Expand Down Expand Up @@ -252,29 +255,32 @@
"@prefix P: <urn:___param___#> .\n",
"@prefix brick: <https://brickschema.org/schema/Brick#> .\n",
"\n",
"<http://example.org/building/5-Zone-PVAV-2> brick:hasPoint P:p1 .\n",
"<http://example.org/building/5-Zone-PVAV> brick:hasPoint P:p1 .\n",
"\n",
"P:p1 a brick:Supply_Air_Temperature_Setpoint .\n",
"\n",
"\n",
"Please enter the value for parameter \"p1\":p1\n",
"--------------------------------------------------------------------------------\n",
"@prefix P: <urn:___param___#> .\n",
"@prefix brick: <https://brickschema.org/schema/Brick#> .\n",
"\n",
"<http://example.org/building/5-Zone-PVAV> brick:hasPoint P:p2 .\n",
"<http://example.org/building/5-Zone-PVAV-1> brick:hasPoint P:p2 .\n",
"\n",
"P:p2 a brick:Supply_Air_Temperature_Setpoint .\n",
"\n",
"\n",
"Please enter the value for parameter \"p2\":p2\n",
"--------------------------------------------------------------------------------\n",
"@prefix P: <urn:___param___#> .\n",
"@prefix brick: <https://brickschema.org/schema/Brick#> .\n",
"\n",
"<http://example.org/building/5-Zone-PVAV-1> brick:hasPoint P:p3 .\n",
"<http://example.org/building/5-Zone-PVAV-2> brick:hasPoint P:p3 .\n",
"\n",
"P:p3 a brick:Supply_Air_Temperature_Setpoint .\n",
"\n",
"\n"
"\n",
"Please enter the value for parameter \"p3\":p3\n"
]
}
],
Expand Down Expand Up @@ -375,10 +381,12 @@
"\t Conformance: True\n",
"Fault Condition: Too many changes in Operating State\n",
"\t Conformance: False\n",
"\t Reasons why:\n",
"\t- http://example.org/building/5-Zone-PVAV-2 needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"\t- http://example.org/building/5-Zone-PVAV-1 needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"\t- http://example.org/building/5-Zone-PVAV needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"http://example.org/building/5-Zone-PVAV-1\n",
" - http://example.org/building/5-Zone-PVAV-1 needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"http://example.org/building/5-Zone-PVAV-2\n",
" - http://example.org/building/5-Zone-PVAV-2 needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"http://example.org/building/5-Zone-PVAV\n",
" - http://example.org/building/5-Zone-PVAV needs between 1 and None instances of https://brickschema.org/schema/Brick#Operating_Mode_Status on path https://brickschema.org/schema/Brick#hasPoint\n",
"Fault Condition: SAT too low; should be higher than MAT\n",
"\t Conformance: True\n",
"Fault Condition: OA fraction is too low or too high; should equal %OA_min\n",
Expand All @@ -396,9 +404,10 @@
" print(f\"Fault Condition: {label}\")\n",
" print(f\"\\t Conformance: {validation_context.valid}\")\n",
" if not validation_context.valid:\n",
" print(\"\\t Reasons why:\")\n",
" for diff in validation_context.diffset:\n",
" print(f\"\\t- {diff.reason()}\")"
" for focus_node, diffs in validation_context.diffset.items():\n",
" print(focus_node)\n",
" for diff in diffs:\n",
" print(\" - \" + diff.reason())"
]
},
{
Expand Down Expand Up @@ -429,7 +438,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.0"
"version": "3.10.0"
}
},
"nbformat": 4,
Expand Down
Loading