diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index fa2e5349a7..1ec889309d 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -232,7 +232,7 @@ class Attribute(Facet): elif isinstance(self.value, str): cast_value = cast_to_value(self.value, value) if isinstance(value, float) and isinstance(cast_value, float): - if value < cast_value * (1. - 1e-6) or value > cast_value * (1. + 1e-6): + if value < cast_value * (1.0 - 1e-6) or value > cast_value * (1.0 + 1e-6): is_pass = False reason = {"type": "VALUE", "actual": value} break @@ -295,28 +295,79 @@ class Classification(Facet): class PartOf(Facet): - def __init__(self, entity="IfcSystem"): - self.parameters = ["@entity"] - self.applicability_templates = ["An element part of a {entity}"] - self.requirement_templates = ["Must be part of a {entity}"] - super().__init__(entity) + def __init__(self, entity=None, relation="IfcRelAggregates", instructions=None): + self.parameters = ["entity", "@relation", "@instructions"] + self.applicability_templates = [ + "An element with an {relation} relationship with an {entity}", + "An element with an {relation} relationship", + ] + self.requirement_templates = [ + "An element must have an {relation} relationship with an {entity}", + "An element must have an {relation} relationship", + ] + super().__init__(entity, relation, instructions) def __call__(self, inst, logger=None): - if self.entity == "IfcElementAssembly": - is_pass = False + reason = None + if self.relation == "IfcRelAggregates": aggregate = ifcopenshell.util.element.get_aggregate(inst) - while aggregate is not None: - if aggregate.is_a() == "IfcElementAssembly": - is_pass = True - break - aggregate = ifcopenshell.util.element.get_aggregate(aggregate) - else: - is_pass = False + is_pass = aggregate is not None + if not is_pass: + reason = {"type": "RELATION"} + if is_pass and self.entity: + is_pass = False + ancestors = [] + while aggregate is not None: + ancestors.append(aggregate.is_a()) + if aggregate.is_a().upper() == self.entity: + is_pass = True + break + aggregate = ifcopenshell.util.element.get_aggregate(aggregate) + if not is_pass: + reason = {"type": "ENTITY", "actual": ancestors} + elif self.relation == "IfcRelAssignsToGroup": + group = None for rel in getattr(inst, "HasAssignments", []) or []: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity): - is_pass = True + if rel.is_a("IfcRelAssignsToGroup"): + group = rel.RelatingGroup + break + is_pass = group is not None + if not is_pass: + reason = {"type": "NOVALUE"} + if is_pass and self.entity: + if group.is_a().upper() != self.entity: + is_pass = False + reason = {"type": "ENTITY", "actual": group.is_a().upper()} + elif self.relation == "IfcRelContainedInSpatialStructure": + container = ifcopenshell.util.element.get_container(inst) + is_pass = container is not None + if not is_pass: + reason = {"type": "RELATION"} + if is_pass and self.entity: + if container.is_a().upper() != self.entity: + is_pass = False + reason = {"type": "ENTITY", "actual": container.is_a().upper()} + elif self.relation == "IfcRelNests": + nest = self.get_nested_whole(inst) + is_pass = nest is not None + if not is_pass: + reason = {"type": "NOVALUE"} + if is_pass and self.entity: + is_pass = False + ancestors = [] + while nest is not None: + ancestors.append(nest.is_a()) + if nest.is_a().upper() == self.entity: + is_pass = True + break + nest = self.get_nested_whole(nest) + if not is_pass: + reason = {"type": "ENTITY", "actual": ancestors} + return PartOfResult(is_pass, reason) - return PartOfResult(is_pass, "TODO") + def get_nested_whole(self, element): + for rel in getattr(element, "Nests", []) or []: + return rel.RelatingObject class Property(Facet): @@ -426,7 +477,7 @@ class Property(Facet): elif isinstance(self.value, str): cast_value = cast_to_value(self.value, value) if isinstance(value, float) and isinstance(cast_value, float): - if value < cast_value * (1. - 1e-6) or value > cast_value * (1. + 1e-6): + if value < cast_value * (1.0 - 1e-6) or value > cast_value * (1.0 + 1e-6): is_pass = False reason = {"type": "VALUE", "actual": value} break diff --git a/src/ifctester/ifctester/ids.xsd b/src/ifctester/ifctester/ids.xsd index 884cc780ad..d7bef60b02 100644 --- a/src/ifctester/ifctester/ids.xsd +++ b/src/ifctester/ifctester/ids.xsd @@ -56,9 +56,16 @@ + + + + + + + @@ -115,17 +122,16 @@ - - - - - - - - - - - + + + + + + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. + + + + @@ -242,4 +248,4 @@ - \ No newline at end of file + diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index 3b7f5e67d7..d481091796 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -492,25 +492,25 @@ class TestAttribute: run( "Floating point numbers are compared with a 1e-6 tolerance 1/4", facet=facet, - inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. + 1e-6)), + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 + 1e-6)), expected=True, ) run( "Floating point numbers are compared with a 1e-6 tolerance 2/4", facet=facet, - inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. - 1e-6)), + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 - 1e-6)), expected=True, ) run( "Floating point numbers are compared with a 1e-6 tolerance 3/4", facet=facet, - inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. + 2e-6)), + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 + 2e-6)), expected=False, ) run( "Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, - inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42. * (1. - 2e-6)), + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0 * (1.0 - 2e-6)), expected=False, ) @@ -931,13 +931,21 @@ class TestProperty: run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True) facet = Property(propertySet="Foo_Bar", name="Foo", value="42.") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. + 1e-6))}) + ifcopenshell.api.run( + "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 1e-6))} + ) run("Floating point numbers are compared with a 1e-6 tolerance 1/4", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. - 1e-6))}) + ifcopenshell.api.run( + "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 - 1e-6))} + ) run("Floating point numbers are compared with a 1e-6 tolerance 2/4", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. + 2e-6))}) + ifcopenshell.api.run( + "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 2e-6))} + ) run("Floating point numbers are compared with a 1e-6 tolerance 3/4", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42. * (1. - 2e-6))}) + ifcopenshell.api.run( + "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 - 2e-6))} + ) run("Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, inst=element, expected=False) facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE") @@ -1017,9 +1025,19 @@ class TestProperty: element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)}) - run("Unit conversions shall take place to IDS-nominated standard units 1/2", facet=facet, inst=element, expected=False) + run( + "Unit conversions shall take place to IDS-nominated standard units 1/2", + facet=facet, + inst=element, + expected=False, + ) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)}) - run("Unit conversions shall take place to IDS-nominated standard units 2/2", facet=facet, inst=element, expected=True) + run( + "Unit conversions shall take place to IDS-nominated standard units 2/2", + facet=facet, + inst=element, + expected=True, + ) wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") @@ -1197,69 +1215,102 @@ class TestMaterial: class TestPartOf: def test_creating_a_partof_facet(self): facet = PartOf() - assert facet.asdict() == {"@entity": "IfcSystem"} - facet = PartOf(entity="IfcGroup") - assert facet.asdict() == {"@entity": "IfcGroup"} + assert facet.asdict() == {"@relation": "IfcRelAggregates"} + facet = PartOf(entity="IfcGroup", relation="IfcRelAssignsToGroup", instructions="instructions") + assert facet.asdict() == { + "entity": {"simpleValue": "IfcGroup"}, + "@relation": "IfcRelAssignsToGroup", + "@instructions": "instructions", + } def test_filtering_using_a_partof_facet(self): ifc = ifcopenshell.file() - # An IfcElementAssembly entity only passes those who are part of an assembly element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + facet = PartOf(relation="IfcRelAggregates") + run("A non aggregated element fails an aggregate relationship", facet=facet, inst=subelement, expected=False) ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) - facet = PartOf(entity="IfcElementAssembly") - run("", facet=facet, inst=element, expected=False) - run("", facet=facet, inst=subelement, expected=True) + run("The aggregated whole fails an aggregate relationship", facet=facet, inst=element, expected=False) + run("The aggregated part passes an aggregate relationship", facet=facet, inst=subelement, expected=True) - # An IfcElementAssembly strictly checks that the whole is an IfcElementAssembly class element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) - facet = PartOf(entity="IfcElementAssembly") - run("", facet=facet, inst=subelement, expected=False) + facet = PartOf(entity="IFCSLAB", relation="IfcRelAggregates") + run("An aggregate may specify the entity of the whole 1/2", facet=facet, inst=subelement, expected=True) + facet = PartOf(entity="IFCWALL", relation="IfcRelAggregates") + run("An aggregate may specify the entity of the whole 2/2", facet=facet, inst=subelement, expected=False) - # A nested subelement still passes so long as one of its parents is an IfcElementAssembly - # TODO nononono element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) ifcopenshell.api.run("aggregate.assign_object", ifc, product=subsubelement, relating_object=subelement) - facet = PartOf(entity="IfcElementAssembly") - run("", facet=facet, inst=subsubelement, expected=True) + facet = PartOf(entity="IFCELEMENTASSEMBLY", relation="IfcRelAggregates") + run("An aggregate entity may pass any ancestral whole passes", facet=facet, inst=subsubelement, expected=True) - # An IfcGroup only checks that a group is assigned without any other logic element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") group = ifcopenshell.api.run("group.add_group", ifc) - facet = PartOf(entity="IfcGroup") - run("", facet=facet, inst=element, expected=False) + facet = PartOf(relation="IfcRelAssignsToGroup") + run("A non grouped element fails a group relationship", facet=facet, inst=element, expected=False) ifcopenshell.api.run("group.assign_group", ifc, products=[element], group=group) - run("", facet=facet, inst=element, expected=True) + run("A grouped element passes a group relationship", facet=facet, inst=element, expected=True) - # An IfcGroup can be passed by subtypes - # TODO: wrong, subtypes should not be matched element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") group = ifc.createIfcInventory() - facet = PartOf(entity="IfcGroup") + facet = PartOf(entity="IFCGROUP", relation="IfcRelAssignsToGroup") ifcopenshell.api.run("group.assign_group", ifc, products=[element], group=group) - run("", facet=facet, inst=element, expected=True) + run("A group entity must match exactly 1/2", facet=facet, inst=element, expected=False) + facet = PartOf(entity="IFCINVENTORY", relation="IfcRelAssignsToGroup") + run("A group entity must match exactly 2/2", facet=facet, inst=element, expected=True) - # An IfcSystem only checks that a system is assigned without any other logic element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") - system = ifcopenshell.api.run("system.add_system", ifc) - facet = PartOf(entity="IfcSystem") - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system) - run("", facet=facet, inst=element, expected=True) + container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") + facet = PartOf(relation="IfcRelContainedInSpatialStructure") + run("Any contained element passes a containment relationship 1/2", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + run("Any contained element passes a containment relationship 2/2", facet=facet, inst=element, expected=True) + run("The container itself always fails", facet=facet, inst=container, expected=False) - # An IfcSystem allows subtypes - # TODO: wrong, subtypes should not be matched element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") - system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcDistributionSystem") - ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system) - facet = PartOf(entity="IfcSystem") - run("", facet=facet, inst=element, expected=True) + container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") + ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSITE") + run("The container entity must match exactly 1/2", facet=facet, inst=element, expected=False) + facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE") + run("The container entity must match exactly 2/2", facet=facet, inst=element, expected=True) + + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") + ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") + ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + facet = PartOf(relation="IfcRelContainedInSpatialStructure", entity="IFCSPACE") + run("The container may be indirect", facet=facet, inst=subelement, expected=True) + + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") + ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) + facet = PartOf(relation="IfcRelNests") + run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True) + run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False) + + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") + ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) + facet = PartOf(relation="IfcRelNests", entity="IFCBEAM") + run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False) + facet = PartOf(relation="IfcRelNests", entity="IFCFURNITURE") + run("The nest entity must match exactly 2/2", facet=facet, inst=subelement, expected=True) + + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") + subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") + subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener") + ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) + ifcopenshell.api.run("nest.assign_object", ifc, related_object=subsubelement, relating_object=subelement) + facet = PartOf(relation="IfcRelNests", entity="IFCFURNITURE") + run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True) class TestRestriction: