From 7329c485ea0e9033bf7fdcf8b0217b9ef9080c55 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 31 Oct 2025 10:03:52 -0500 Subject: [PATCH] Fix quantify() regression where by_type queries and grouped iteration could apply QTOs to all entities in the model instead of the intended element subset. --- src/ifc5d/ifc5d/qto.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 8f21fa4235..899861868a 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -92,24 +92,38 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst for calculator, queries in rules["calculators"].items(): calculator = calculators[calculator] - if not set(m.lower() for m in queries.keys()) - lower_case_entity_names(ifc_file.schema_identifier): - # all defined queries are actually simple entity names: instead of looping over all - # calculators we loop over the entity types so that we don't have to repeatedly query - # the model, especially when the set of elements is small. + # Cache schema entity names once per file + schema_entity_names = lower_case_entity_names(ifc_file.schema_identifier) + + # Fast path: all queries are simple entity names + if not set(m.lower() for m in queries.keys()) - schema_entity_names: casenorm = {k.lower(): k for k in queries.keys()} pred = lambda inst: inst.is_a().lower() - for ty, elements in itertools.groupby(sorted(elements, key=pred), key=pred): + + for ty, group in itertools.groupby(sorted(elements, key=pred), key=pred): + group_elements = list(group) + # check entity type and its supertypes for matching QTO rules for sty in entity_supertypes(ifc_file.schema_identifier, ty): if qtos := queries.get(casenorm.get(sty)): - calculator.calculate(ifc_file, list(elements), qtos, results) + calculator.calculate(ifc_file, group_elements, qtos, results) + + # Fallback: per-query evaluation for query, qtos in queries.items(): - if query.lower() in lower_case_entity_names(ifc_file.schema_identifier): - # by_type is faster than selector parsing - filtered_elements = ifc_file.by_type(query) + # Simple entity name: use by_type but restrict to incoming elements + if query.lower() in schema_entity_names: + by_type_all = ifc_file.by_type(query) + # ensure we don't expand beyond provided elements subset + if isinstance(elements, set): + filtered_elements = [e for e in by_type_all if e in elements] + else: + elements_set = set(elements) + filtered_elements = [e for e in by_type_all if e in elements_set] else: filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements) + if filtered_elements: calculator.calculate(ifc_file, filtered_elements, qtos, results) + return results @@ -159,8 +173,7 @@ class IteratorForTypes: element: Union[ifcopenshell.entity_instance, None] = None shape: Union[ifcopenshell.geom.ShapeType, None] = None - def __init__( - self, + def __init__(self, ifc_file: ifcopenshell.file, settings: ifcopenshell.geom.settings, elements: Iterable[ifcopenshell.entity_instance], @@ -560,4 +573,4 @@ class Blender(QtoCalculator): calculators: dict[str, type[QtoCalculator]] = { "Blender": Blender, "IfcOpenShell": IfcOpenShell, -} +} \ No newline at end of file