diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py index e500bcef8d..655f89f3db 100644 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ b/src/ifcopenshell-python/ifcopenshell/ids.py @@ -25,6 +25,7 @@ import datetime import ifcopenshell.util.element import ifcopenshell.util.placement +import ifcopenshell.util.classification from bcf.v2.bcfxml import BcfXml from bcf.v2 import data as bcf @@ -524,9 +525,7 @@ class entity(facet): if self.predefinedType: self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'" - return facet_evaluation( - is_pass, self.message % {"name": inst.is_a(), "predefinedType": predefined_type} - ) + return facet_evaluation(is_pass, self.message % {"name": inst.is_a(), "predefinedType": predefined_type}) else: self.message = "an entity name '%(name)s'" return facet_evaluation(is_pass, self.message % {"name": inst.is_a()}) @@ -590,6 +589,7 @@ class attribute(facet): :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ + def get_values(element, name): if isinstance(name, str): return [getattr(element, name, None)] @@ -675,7 +675,7 @@ class classification(facet): results["@instructions"] = self.instructions return results - def __call__(self, inst, logger): + def __call__(self, inst, logger=None): """Validate an ifc instance against that classification facet. :param inst: IFC entity element @@ -685,42 +685,39 @@ class classification(facet): :return: result of the validation as bool and message :rtype: facet_evaluation(bool, str) """ + if self.location == "instance": + leaf_references = ifcopenshell.util.classification.get_references(inst, should_inherit=False) + elif self.location == "type": + element_type = ifcopenshell.util.element.get_type(inst) + leaf_references = ifcopenshell.util.classification.get_references(element_type) if element_type else set() + elif self.location == "any": + leaf_references = ifcopenshell.util.classification.get_references(inst) - instance_classiciations = inst.HasAssociations - if ifcopenshell.util.element.get_type(inst): - type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations - else: - type_classifications = () + references = leaf_references.copy() + for leaf_reference in leaf_references: + references.update(ifcopenshell.util.classification.get_inherited_references(leaf_reference)) - if self.location == "instance" and instance_classiciations: - associations = instance_classiciations - elif self.location == "type" and type_classifications: - associations = type_classifications - elif self.location == "any" and (instance_classiciations or type_classifications): - associations = instance_classiciations + type_classifications - else: - associations = () - - refs = [] - for association in associations: - if association.is_a("IfcRelAssociatesClassification"): - cref = association.RelatingClassification - if hasattr(cref, "ItemReference"): # IFC2x3 - refs.append((cref.ReferencedSource.Name, cref.ItemReference)) - elif hasattr(cref, "Identification"): # IFC4 - refs.append((cref.ReferencedSource.Name, cref.Identification)) + is_pass = bool(references) + if is_pass and self.value: + is_pass = any( + [self.value == getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references] + ) + if is_pass and self.system: + is_pass = any( + [self.system == ifcopenshell.util.classification.get_classification(r).Name for r in references] + ) self.location_msg = location[self.location] - if refs: + if references: return facet_evaluation( - (self.system, self.value) in refs, + is_pass, self.message % { - "system": refs[0][0], - "value": "'" + refs[0][1] + "'", + "system": list(references)[0][0], + "value": list(references)[0][1], "location": self.location_msg, - }, # what if not first item of refs? + }, # TODO Fix this 0 index reference assumption when I refactor out the messages ) else: return facet_evaluation(False, "does not have %sclassification reference" % self.location_msg) diff --git a/src/ifcopenshell-python/test/test_ids.py b/src/ifcopenshell-python/test/test_ids.py index d247efc9be..18056989fe 100644 --- a/src/ifcopenshell-python/test/test_ids.py +++ b/src/ifcopenshell-python/test/test_ids.py @@ -480,6 +480,128 @@ class TestIdsAuthoring(unittest.TestCase): "@instructions": "instructions", } + def test_filtering_using_a_classification_facet(self): + library = ifcopenshell.file() + system = library.createIfcClassification(Name="Foobar") + ref1 = library.createIfcClassificationReference(Identification="1", ReferencedSource=system) + ref11 = library.createIfcClassificationReference(Identification="11", ReferencedSource=ref1) + ref2 = library.createIfcClassificationReference(Identification="2", ReferencedSource=system) + ref22 = library.createIfcClassificationReference(Identification="22", ReferencedSource=ref2) + + ifc = ifcopenshell.file() + project = ifc.createIfcProject() + ifcopenshell.api.run("classification.add_classification", ifc, classification=system) + element0 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + element1 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", ifc, product=element1, reference=ref1, classification=system + ) + element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", ifc, product=element11, reference=ref11, classification=system + ) + element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", + ifc, + product=element22, + reference=ref22, + classification=system, + is_lightweight=False, + ) + + # A classification facet with no data matches any present classification + facet = ids.classification.create() + assert bool(facet(element0)) is False + assert bool(facet(element1)) is True + + # Values should match exactly if lightweight classifications are used. + facet = ids.classification.create(value="1") + assert bool(facet(element1)) is True + + # Values should match subreferences if full classifications are used. + # E.g. a facet searching for Uniclass EF_25_10 Walls will match Uniclass EF_25_10_25, EF_25_10_30, etc + facet = ids.classification.create(value="2") + assert bool(facet(element22)) is True + + # Systems should match exactly regardless of lightweight or full classifications + facet = ids.classification.create(system="Foobar") + assert bool(facet(project)) is True + assert bool(facet(element0)) is False + assert bool(facet(element1)) is True + assert bool(facet(element11)) is True + assert bool(facet(element22)) is True + + # Restrictions can be used for values + restriction = ids.restriction.create(options="1.*", type="pattern", base="string") + facet = ids.classification.create(value=restriction) + assert bool(facet(element1)) is True + assert bool(facet(element11)) is True + assert bool(facet(element22)) is False + + # Restrictions can be used for systems + restriction = ids.restriction.create(options="Foo.*", type="pattern", base="string") + facet = ids.classification.create(system=restriction) + assert bool(facet(element0)) is False + assert bool(facet(element1)) is True + + # Specifying both a value and a system means that both (as opposed to either) requirements must be met + facet = ids.classification.create(system="Foobar", value="1") + assert bool(facet(element1)) is True + assert bool(facet(element11)) is False + + # Location instance only checks on the instance (even if it's a type), this seems strange though + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall_type, reference=ref1, classification=system + ) + facet = ids.classification.create(value="1", location="instance") + assert bool(facet(wall_type)) is True + assert bool(facet(wall)) is False + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall, reference=ref1, classification=system + ) + assert bool(facet(wall)) is True + + # Location type only checks on the type + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall, reference=ref1, classification=system + ) + facet = ids.classification.create(value="1", location="type") + assert bool(facet(wall)) is False + assert bool(facet(wall_type)) is False + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall_type, reference=ref1, classification=system + ) + assert bool(facet(wall)) is True + assert bool(facet(wall_type)) is True + + # Location any checks on either the type or instance. + # IFC doesn't specify how inheritance and overrides work here. Two options: + # Option 1) Occurrences replace inherited type references + # Option 2) Occurrences union with inherited type references + # Option 2 is followed here. + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall, reference=ref11, classification=system + ) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system + ) + facet = ids.classification.create(value="11", location="any") + assert bool(facet(wall)) is True + assert bool(facet(wall_type)) is False + facet = ids.classification.create(value="22", location="any") + assert bool(facet(wall)) is True + assert bool(facet(wall_type)) is True + def test_property_create(self): p = ids.property.create( location="any", propertySet="Test_PropertySet", name="Test_Parameter", value="Test_Value"