diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a332f7..d522e05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,7 +25,7 @@ jobs: python-version: "3.10" - name: Install test dependencies - run: python -m pip install "ifcopenshell[advanced]" pytest pyparsing + run: python -m pip install "ifcopenshell[advanced]" pytest pyparsing rdflib tabulate xlsxwriter - name: Install the checked-out MVD modules shell: bash diff --git a/README.md b/README.md index 9535dab..53c2158 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ concept templates returns a tuple of `template` objects instead. Invalid XML and missing template references raise `ValueError` with the relevant document or template identifier. Recursive template branches are expanded once. +When a referenced template's `applicableEntity` differs from its anchor entity, +the parsed tree includes an additional `EntityRule` for that type. Consecutive +entity rules apply to the same IFC instance; sibling entity rules are alternative +specializations. Matching roots are expanded without an extra rule. This keeps +type restrictions visible to traversal, extraction and SPARQL conversion. +Serialization writes these restrictions as partial-template references again. + ### Graphviz concept templates The lightweight concept graphs used by the buildingSMART IFC4.x documentation diff --git a/graphviz.py b/graphviz.py index a75e5dd..f046b17 100644 --- a/graphviz.py +++ b/graphviz.py @@ -76,15 +76,18 @@ def parse( root = min(graph.in_degree(), key=lambda item: item[1])[0] resolved_references = _reference_lookup(references or {}) - def build(node: str, top_level_attribute: bool = False) -> tuple[rule, ...]: + def build(node: str, top_level_attribute: bool = False, anchor: str = "") -> tuple[rule, ...]: node_type = graph.nodes[node].get("type", "EntityRule") if node_type == "Reference": try: - return resolved_references[_normalise_reference(node)].rules + referenced = resolved_references[_normalise_reference(node)] except KeyError as error: raise ValueError( f"Unknown Graphviz template reference: {node}" ) from error + if referenced.entity and referenced.entity.casefold() != anchor.casefold(): + return (rule("EntityRule", referenced.entity, referenced.rules),) + return referenced.rules if node_type == "Constraint": try: @@ -99,6 +102,7 @@ def build(node: str, top_level_attribute: bool = False) -> tuple[rule, ...]: itertools.chain.from_iterable( build( child, + anchor=node.split("_", 1)[0] if node_type == "EntityRule" else anchor, top_level_attribute=( node == root and graph.nodes[child].get("type") == "AttributeRule" diff --git a/model.py b/model.py index 8dda104..02067b1 100644 --- a/model.py +++ b/model.py @@ -167,7 +167,22 @@ def extract(self, ifc_data: ifcopenshell.entity_instance | Any) -> extracted_dat return [] to_combine: list[extracted_data] = [] + # Sibling entity rules represent alternative specializations of + # this instance, just as entity rules under an attribute do. + entity_alternatives = [ + child for child in self.nodes if child.tag == "EntityRule" + ] + if entity_alternatives: + to_combine.append( + [ + values + for child in entity_alternatives + for values in child.extract(ifc_data) + ] + ) for child in self.nodes: + if child.tag == "EntityRule": + continue if child.tag == "Constraint": on_node = child.attribute[0][0].c.replace("'", "") if isinstance(ifc_data, ifcopenshell.entity_instance): @@ -249,12 +264,18 @@ def extract(self, ifc_data: ifcopenshell.entity_instance | Any) -> extracted_dat def binding_for(self, target: rule) -> str | None: binding: str | None = None - def find(rule: rule, parent: rule | None) -> None: + def find(rule: rule, parents: tuple[rule | None, ...]) -> None: nonlocal binding if rule is target: - binding = rule.bind or (parent.bind if parent else None) - - self.traverse(find) + binding = rule.bind + for parent in reversed(parents): + if binding or parent is None: + break + binding = parent.bind + if parent.tag != "EntityRule": + break + + self.traverse(find, with_parents=True) return binding diff --git a/mvd.py b/mvd.py index 6f3d83d..bb9c84e 100644 --- a/mvd.py +++ b/mvd.py @@ -1,5 +1,6 @@ import ifcopenshell import ifcopenshell.geom +from .model import rule import itertools import os @@ -36,6 +37,9 @@ def extract_data(mvd_node, ifc_data): """ + if isinstance(mvd_node, rule): + return mvd_node.extract(ifc_data) + to_combine = [] return_value = [] diff --git a/parser.py b/parser.py index 663cd77..6401a97 100644 --- a/parser.py +++ b/parser.py @@ -42,6 +42,7 @@ def parse_template( parsed_rules: list[rule] = [] next_visited = visited | {template_key} + entity = root.getAttribute("applicableEntity") # Only the template's own are in scope. Nested # form a hierarchy of separate templates, so they must not contribute # rules (and therefore entity scope) to the parent template. @@ -51,7 +52,7 @@ def parse_template( for node in _elements(rules_node): # A child unions one or more