From ce8a9293bbc9b9338dc99f1d1fd326b8140a3342 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 4 Jul 2023 15:50:29 +1000 Subject: [PATCH] See #3334. Optimise pset getting and add simple "broadphase" filtering for all facets for faster validation. --- .../ifcopenshell/util/element.py | 11 +++-- src/ifctester/ifctester/facet.py | 48 +++++++++++++++++-- src/ifctester/ifctester/ids.py | 12 ++--- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index baf318ad45..5d74bcc34b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -77,7 +77,9 @@ def get_pset(element, name, prop=None, should_inherit=True): if not prop: if type_pset: - type_pset.update(get_property_definition(pset)) + occurrence_pset = get_property_definition(pset) + if occurrence_pset: + type_pset.update(occurrence_pset) return type_pset return get_property_definition(pset) @@ -128,9 +130,10 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True): continue psets[definition.Name] = get_property_definition(definition) elif hasattr(element, "IsDefinedBy"): - element_type = ifcopenshell.util.element.get_type(element) - if element_type and should_inherit: - psets = get_psets(element_type, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False) + if should_inherit: + element_type = ifcopenshell.util.element.get_type(element) + if element_type: + psets = get_psets(element_type, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False) for relationship in element.IsDefinedBy: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 1a7b2357a3..9a45522218 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -131,6 +131,9 @@ class Entity(Facet): super().__init__(name, predefinedType, instructions) def filter(self, ifc_file, elements): + if isinstance(elements, list): + return super().filter(ifc_file, elements) + if isinstance(self.name, str): try: results = ifc_file.by_type(self.name, include_subtypes=False) @@ -180,6 +183,21 @@ class Attribute(Facet): ] super().__init__(name, value, minOccurs, maxOccurs, instructions) + def filter(self, ifc_file, elements): + if isinstance(elements, list): + return super().filter(ifc_file, elements) + + results = [] + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + for entity in schema.entities(): + for attribute in entity.attributes(): + if attribute.name() == self.name: + results.extend(ifc_file.by_type(entity.name(), include_subtypes=False)) + + # TODO: perhaps we should consider value in the filter + + return results + def __call__(self, inst, logger=None): if self.minOccurs == 0 and self.maxOccurs != 0: return AttributeResult(True) @@ -285,7 +303,9 @@ class Classification(Facet): super().__init__(value, system, uri, minOccurs, maxOccurs, instructions) def filter(self, ifc_file, elements): - pass + if isinstance(elements, list): + return super().filter(ifc_file, elements) + return ifc_file.by_type("IfcObjectDefinition") def __call__(self, inst, logger=None): if self.minOccurs == 0 and self.maxOccurs != 0: @@ -341,6 +361,11 @@ class PartOf(Facet): ] super().__init__(entity, predefinedType, relation, minOccurs, maxOccurs, instructions) + def filter(self, ifc_file, elements): + if isinstance(elements, list): + return super().filter(ifc_file, elements) + return list(ifc_file) # Lazy + def asdict(self): results = super().asdict() entity = {} @@ -479,16 +504,26 @@ class Property(Facet): ] super().__init__(propertySet, name, value, measure, uri, minOccurs, maxOccurs, instructions) + def filter(self, ifc_file, elements): + if isinstance(elements, list): + return super().filter(ifc_file, elements) + if ifc_file.schema == "IFC2X3": + return ifc_file.by_type("IfcObjectDefinition") + return ( + ifc_file.by_type("IfcObjectDefinition") + + ifc_file.by_type("IfcMaterialDefinition") + + ifc_file.by_type("IfcProfileDef") + ) + def __call__(self, inst, logger=None): if self.minOccurs == 0 and self.maxOccurs != 0: return PropertyResult(True) - all_psets = ifcopenshell.util.element.get_psets(inst) - if isinstance(self.propertySet, str): - pset = all_psets.get(self.propertySet, None) + pset = ifcopenshell.util.element.get_pset(inst, self.propertySet) psets = {self.propertySet: pset} if pset else {} else: + all_psets = ifcopenshell.util.element.get_psets(inst) psets = {k: v for k, v in all_psets.items() if k == self.propertySet} is_pass = bool(psets) @@ -730,6 +765,11 @@ class Material(Facet): ] super().__init__(value, uri, minOccurs, maxOccurs, instructions) + def filter(self, ifc_file, elements): + if isinstance(elements, list): + return super().filter(ifc_file, elements) + return ifc_file.by_type("IfcObjectDefinition") + def __call__(self, inst, logger=None): if self.minOccurs == 0 and self.maxOccurs != 0: return MaterialResult(True) diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index 6fce983f10..692d35254c 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -200,18 +200,14 @@ class Specification: if filter_version and ifc_file.schema not in self.ifcVersion: return - elements = [] + elements = None + # This is a broadphase filter of applicability. We almost never want to + # test every single class in an IFC model. for i, facet in enumerate(self.applicability): - # Usually, we rely on an entity applicability to give us our first - # shortlist of elements, as it's the most efficient way to filter - # elements. If this does not exist, then we have no choice but to - # check everything. - if i == 0 and not isinstance(facet, Entity): - elements = list(ifc_file) elements = facet.filter(ifc_file, elements) - for element in elements: + for element in elements or []: is_applicable = True for facet in self.applicability: if isinstance(facet, Entity):