From d0fd98ea4eba8a65907125590358790fcbe02d66 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 29 Oct 2025 14:32:08 -0500 Subject: [PATCH] https://matrix.to/#/!ssOmSJysZpXBupsgjO:matrix.org/$7EB0LAxm9eD920ZuafOOPA3mR5grPh0SSWsLXWI1g6w?via=matrix.org&via=tchncs.de&via=crossbach.de --- src/ifc5d/ifc5d/qto.py | 73 ++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 8f21fa4235..d08123aa5d 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -59,15 +59,15 @@ for name in get_args(RULE_SET): with open(os.path.join(cwd, name + ".json"), "r") as f: rules[name] = json.load(f) - @functools.cache def lower_case_entity_names(schema_iden: str): schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_iden) - return set(n.name().lower() for n in schema.entities()) + return {n.name().lower() for n in schema.entities()} @functools.cache def entity_supertypes(schema_iden: str, entity_name: str): + """Return all supertypes (lowercased) for a given entity.""" def visit(decl): yield decl.name().lower() if ty := decl.supertype(): @@ -78,41 +78,52 @@ def entity_supertypes(schema_iden: str, entity_name: str): def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> ResultsDict: - """Quantify elements from a rules using preset quantification rules - - Rules placed as a JSON configuration file in the ``ifc5d`` folder will be - autodetected and loaded with the module for convenience. - - :param rules: Set of rules from `ifc5d.qto.rules`. - """ + """Quantify elements from a rule set using preset quantification rules.""" results: ResultsDict = {} - elements_by_classes: defaultdict[str, set[ifcopenshell.entity_instance]] = defaultdict(set) - for element in elements: - elements_by_classes[element.is_a()].add(element) - 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. + # Precompute entity names once + schema_id = ifc_file.schema_identifier + entity_names_lower = lower_case_entity_names(schema_id) + + # Pre-group elements by type (avoids sorting/groupby) + elements_by_class: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) + for element in elements: + elements_by_class[element.is_a().lower()].append(element) + + for calculator_name, queries in rules["calculators"].items(): + calculator = calculators[calculator_name] + queries_lower = {k.lower(): v for k, v in queries.items()} + + # Determine if all queries are direct entity types (simple case) + if not (set(queries_lower.keys()) - entity_names_lower): 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 sty in entity_supertypes(ifc_file.schema_identifier, ty): - if qtos := queries.get(casenorm.get(sty)): - calculator.calculate(ifc_file, list(elements), qtos, results) - 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) - else: - filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements) - if filtered_elements: - calculator.calculate(ifc_file, filtered_elements, qtos, results) + # Cache supertypes per entity to avoid recomputation + supertype_cache = {} + + for entity_type, elems in elements_by_class.items(): + if entity_type not in supertype_cache: + supertype_cache[entity_type] = entity_supertypes(schema_id, entity_type) + + for sty in supertype_cache[entity_type]: + if (sty in casenorm) and (qtos := queries.get(casenorm[sty])): + calculator.calculate(ifc_file, elems, qtos, results) + else: + # Fallback: complex queries using selector + for query, qtos in queries.items(): + if query.lower() in entity_names_lower: + filtered_elements = ifc_file.by_type(query) + 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 + + + def edit_qtos(ifc_file: ifcopenshell.file, results: ResultsDict) -> None: """Apply quantification results as quantity sets.""" for element, qtos in results.items():