Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand Down
29 changes: 25 additions & 4 deletions model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down
4 changes: 4 additions & 0 deletions mvd.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ifcopenshell
import ifcopenshell.geom
from .model import rule

import itertools
import os
Expand Down Expand Up @@ -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 = []

Expand Down
17 changes: 14 additions & 3 deletions parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Rules> are in scope. Nested <SubTemplates>
# form a hierarchy of separate templates, so they must not contribute
# rules (and therefore entity scope) to the parent template.
Expand All @@ -51,7 +52,7 @@ def parse_template(
for node in _elements(rules_node):
# A <References> child unions one or more <Template>
# references, so a single child may expand to several rules.
parsed_rules.extend(self._visit_rule(node, "", next_visited))
parsed_rules.extend(self._visit_rule(node, "", next_visited, entity))

return template(
entity=(
Expand All @@ -70,12 +71,14 @@ def _visit_rule(
node: Element,
prefix: str,
visited: frozenset[str],
anchor: str,
) -> tuple[rule, ...]:
attribute: Any = None
bind: str | None = None
optional = False
target = node
child_prefix = prefix
tag = node.localName

if node.localName == "AttributeRule":
attribute = node.attributes["AttributeName"].value
Expand All @@ -89,6 +92,7 @@ def _visit_rule(
)
elif node.localName == "EntityRule":
attribute = node.attributes["EntityName"].value
anchor = attribute
elif node.localName == "Template":
reference = node.attributes["ref"].value
reference_key = reference.casefold()
Expand All @@ -103,6 +107,13 @@ def _visit_rule(
if "IdPrefix" in node.attributes:
child_prefix += node.attributes["IdPrefix"].value
visited = visited | {reference_key}
entity = target.getAttribute("applicableEntity")
if entity and entity.casefold() != anchor.casefold():
# A reference may specialize its anchor without following an
# attribute. Keep that type restriction in the rule tree.
tag = "EntityRule"
attribute = entity
anchor = entity
elif node.localName == "Constraint":
attribute = mvdxml_expression.parse(node.attributes["Expression"].value)
elif node.localName in {
Expand All @@ -121,13 +132,13 @@ def _visit_rule(
children = tuple(
parsed
for child in _elements(target)
for parsed in self._visit_rule(child, child_prefix, visited)
for parsed in self._visit_rule(child, child_prefix, visited, anchor)
)

if attribute is not None:
return (
rule(
tag=node.localName,
tag=tag,
attribute=attribute,
nodes=children,
bind=child_prefix + bind if bind else None,
Expand Down
36 changes: 31 additions & 5 deletions serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def token(value: mvdxml_expression.node | str) -> str:
}


def _rule(parent: ET.Element, value: rule) -> None:
def _rule(parent: ET.Element, value: rule, referenced: list[tuple[str, template]]) -> None:
attributes: dict[str, str] = {}
if value.tag == "AttributeRule":
attributes = {"AttributeName": str(value.attribute)}
Expand All @@ -67,12 +67,30 @@ def _rule(parent: ET.Element, value: rule) -> None:

element = _add(parent, value.tag, **attributes)
for tag, children in groupby(value.nodes, key=lambda child: child.tag):
if value.tag == "EntityRule" and tag == "EntityRule":
# Consecutive entity rules are an in-memory type restriction.
# Write them as partial-template references in mvdXML.
container = _add(element, "References")
for child in children:
identifier = str(uuid.uuid4())
_add(container, "Template", ref=identifier)
referenced.append(
(identifier, template(child.attribute, None, child.nodes))
)
continue
container = _add(element, _CONTAINERS[tag])
for child in children:
_rule(container, child)
_rule(container, child, referenced)


def _template(parent: ET.Element, value: template, identifier: str) -> None:
def _template(
parent: ET.Element,
value: template,
identifier: str,
referenced: list[tuple[str, template]],
*,
partial: bool = False,
) -> None:
element = _add(
parent,
"ConceptTemplate",
Expand All @@ -81,11 +99,12 @@ def _template(parent: ET.Element, value: template, identifier: str) -> None:
status="sample",
applicableSchema="IFC4",
applicableEntity=value.entity,
isPartial="true" if partial else None,
)
if value.rules:
rules = _add(element, "Rules")
for item in value.rules:
_rule(rules, item)
_rule(rules, item, referenced)


def _key(value: template) -> str | int:
Expand Down Expand Up @@ -138,8 +157,15 @@ def serialize(items: Iterable[mvd_item], destination: str | Path) -> None:
{"uuid": str(uuid.uuid4()), "name": "", "status": "sample"},
)
template_elements = _add(document, "Templates")
referenced: list[tuple[str, template]] = []
for key, value in templates.items():
_template(template_elements, value, template_ids[key])
_template(template_elements, value, template_ids[key], referenced)
# Serializing a partial template can discover further specializations.
index = 0
while index < len(referenced):
identifier, value = referenced[index]
_template(template_elements, value, identifier, referenced, partial=True)
index += 1

roots = [item for item in parsed if isinstance(item, concept_root)]
if roots:
Expand Down
21 changes: 18 additions & 3 deletions sparql.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ def withschema(fn):

def _(*args, **kwargs):
schema_name = STANDARD_PREFIXES["ifcowl"].split("/")[-1][:-2]
if "_" in schema_name:
schema_name = schema_name.split("_")[0]
S = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
try:
S = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
except RuntimeError:
if "_" not in schema_name:
raise
S = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name.split("_")[0])
return fn(S, *args, **kwargs)

return _
Expand Down Expand Up @@ -365,6 +368,18 @@ def build(rule, parents):
return_value = lambda: bld.append(INDENT + "}")

if rule.tag == "EntityRule":
siblings = [
child
for child in getattr(parents[-1], "nodes", ())
if child.tag == "EntityRule"
]
if len(siblings) > 1:
bld.append(INDENT + "{")

def close_alternative():
bld.append(INDENT + ("} UNION" if rule is not siblings[-1] else "}"))

return_value = close_alternative
# G.nm = G.next_nm

if not ifcOwl.is_select(rule.attribute):
Expand Down
Loading
Loading