diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 9a45522218..2e5efdf0ae 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -343,9 +343,9 @@ class Classification(Facet): class PartOf(Facet): def __init__( self, - entity=None, + entity="IFCWALL", predefinedType=None, - relation="IfcRelAggregates", + relation=None, minOccurs=None, maxOccurs=None, instructions=None, @@ -390,7 +390,23 @@ class PartOf(Facet): return PartOfResult(True) reason = None - if self.relation == "IfcRelAggregates": + if not self.relation: + is_pass = False + ancestors = [] + parent = self.get_parent(inst) + while parent: + ancestors.append(parent.is_a()) + if parent.is_a().upper() == self.entity: + if self.predefinedType: + if ifcopenshell.util.element.get_predefined_type(parent) == self.predefinedType: + is_pass = True + else: + is_pass = True + break + parent = self.get_parent(parent) + if not is_pass: + reason = {"type": "ENTITY", "actual": ancestors} + elif self.relation == "IFCRELAGGREGATES": aggregate = ifcopenshell.util.element.get_aggregate(inst) is_pass = aggregate is not None if not is_pass: @@ -410,7 +426,7 @@ class PartOf(Facet): aggregate = ifcopenshell.util.element.get_aggregate(aggregate) if not is_pass: reason = {"type": "ENTITY", "actual": ancestors} - elif self.relation == "IfcRelAssignsToGroup": + elif self.relation == "IFCRELASSIGNSTOGROUP": group = None for rel in getattr(inst, "HasAssignments", []) or []: if rel.is_a("IfcRelAssignsToGroup"): @@ -428,7 +444,7 @@ class PartOf(Facet): if predefined_type != self.predefinedType: is_pass = False reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type} - elif self.relation == "IfcRelContainedInSpatialStructure": + elif self.relation == "IFCRELCONTAINEDINSPATIALSTRUCTURE": container = ifcopenshell.util.element.get_container(inst) is_pass = container is not None if not is_pass: @@ -442,7 +458,7 @@ class PartOf(Facet): if predefined_type != self.predefinedType: is_pass = False reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type} - elif self.relation == "IfcRelNests": + elif self.relation == "IFCRELNESTS": nest = self.get_nested_whole(inst) is_pass = nest is not None if not is_pass: @@ -462,6 +478,36 @@ class PartOf(Facet): nest = self.get_nested_whole(nest) if not is_pass: reason = {"type": "ENTITY", "actual": ancestors} + elif self.relation == "IFCRELVOIDSELEMENT": + building_element = self.get_voided_element(inst) + is_pass = building_element is not None + if not is_pass: + reason = {"type": "NOVALUE"} + if is_pass and self.entity: + is_pass = False + if building_element.is_a().upper() == self.entity: + if self.predefinedType: + if ifcopenshell.util.element.get_predefined_type(building_element) == self.predefinedType: + is_pass = True + else: + is_pass = True + if not is_pass: + reason = {"type": "ENTITY", "actual": building_element} + elif self.relation == "IFCRELFILLSELEMENT": + opening = self.filled_opening(inst) + is_pass = opening is not None + if not is_pass: + reason = {"type": "NOVALUE"} + if is_pass and self.entity: + is_pass = False + if opening.is_a().upper() == self.entity: + if self.predefinedType: + if ifcopenshell.util.element.get_predefined_type(opening) == self.predefinedType: + is_pass = True + else: + is_pass = True + if not is_pass: + reason = {"type": "ENTITY", "actual": opening} if self.maxOccurs == 0: return PartOfResult(not is_pass, {"type": "PROHIBITED"}) @@ -471,6 +517,31 @@ class PartOf(Facet): for rel in getattr(element, "Nests", []) or []: return rel.RelatingObject + def get_voided_element(self, element): + for rel in getattr(element, "VoidsElements", []) or []: + return rel.RelatingBuildingElement + + def get_filled_opening(self, element): + for rel in getattr(element, "FillsVoids", []) or []: + return rel.RelatingOpeningElement + + def get_parent(self, element): + parent = ifcopenshell.util.element.get_aggregate(element) + if not parent: + parent = ifcopenshell.util.element.get_container(element, should_get_direct=True) + if not parent: + for rel in getattr(element, "HasAssignments", []) or []: + if rel.is_a("IfcRelAssignsToGroup"): + parent = rel.RelatingGroup + break + if not parent: + self.get_nested_whole(element) + if not parent: + self.get_voided_element(element) + if not parent: + self.get_filled_opening(element) + return parent + class Property(Facet): def __init__( @@ -478,7 +549,7 @@ class Property(Facet): propertySet="Property_Set", name="PropertyName", value=None, - measure=None, + datatype=None, uri=None, minOccurs=None, maxOccurs=None, @@ -488,7 +559,7 @@ class Property(Facet): "propertySet", "name", "value", - "@measure", + "@datatype", "@uri", "@minOccurs", "@maxOccurs", @@ -502,7 +573,7 @@ class Property(Facet): "{name} data shall be {value} and in the dataset {propertySet}", "{name} data shall be provided in the dataset {propertySet}", ] - super().__init__(propertySet, name, value, measure, uri, minOccurs, maxOccurs, instructions) + super().__init__(propertySet, name, value, datatype, uri, minOccurs, maxOccurs, instructions) def filter(self, ifc_file, elements): if isinstance(elements, list): @@ -566,9 +637,9 @@ class Property(Facet): elif prop_entity.is_a("IfcPropertySingleValue"): data_type = prop_entity.NominalValue.is_a() - if data_type != self.measure: + if data_type != self.datatype: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) @@ -585,9 +656,9 @@ class Property(Facet): prop_schema = prop_entity.wrapped_data.declaration().as_entity() data_type = prop_schema.attribute_by_index(3).type_of_attribute().declared_type().name() - if data_type != self.measure: + if data_type != self.datatype: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) @@ -605,9 +676,9 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.EnumerationValues[0].is_a() - if data_type != self.measure: + if data_type != self.datatype: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break elif prop_entity.is_a("IfcPropertyListValue"): if not prop_entity.ListValues: @@ -615,9 +686,9 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.ListValues[0].is_a() - if data_type != self.measure: + if data_type != self.datatype: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) if unit: @@ -638,9 +709,9 @@ class Property(Facet): if value is not None: data_type = value.is_a() values.append(value.wrappedValue) - if data_type != self.measure: + if data_type != self.datatype: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) if unit: @@ -663,7 +734,7 @@ class Property(Facet): if not column_values: continue data_type = column_values[0].is_a() - if data_type == self.measure: + if data_type == self.datatype: column_values = [v.wrappedValue for v in column_values] unit = units[f"{attribute}Unit"] if unit: @@ -680,7 +751,7 @@ class Property(Facet): values.extend(column_values) if not values: is_pass = False - reason = {"type": "MEASURE", "actual": data_type} + reason = {"type": "DATATYPE", "actual": data_type} break props[pset_name][prop_entity.Name] = values else: @@ -963,7 +1034,7 @@ class PropertyResult(Result): return "The required property set does not exist" elif self.reason["type"] == "NOVALUE": return "The property set does not contain the required property" - elif self.reason["type"] == "MEASURE": + elif self.reason["type"] == "DATATYPE": return f"The data type \"{str(self.reason['actual'])}\" does not match the requirements" elif self.reason["type"] == "VALUE": if isinstance(self.reason["actual"], list): diff --git a/src/ifctester/ifctester/ids.xsd b/src/ifctester/ifctester/ids.xsd index 8f0e416d4f..5ac58daf18 100644 --- a/src/ifctester/ifctester/ids.xsd +++ b/src/ifctester/ifctester/ids.xsd @@ -1,8 +1,7 @@ - - - + + - + @@ -27,13 +26,7 @@ - - - - - - - + @@ -58,9 +51,9 @@ - + - + @@ -68,20 +61,8 @@ - - - - - - - - - - - - - - + + @@ -90,11 +71,11 @@ - + - This is the name of an IFC Defined Types. See the documentation and default units on https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md + This is the name of an IFC Defined Type. See the full list for IFC 4 on https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/link/alphabeticalorder-defined-types.htm Documentation and default units on https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md - + @@ -116,7 +97,13 @@ - + + + + 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. + + + @@ -153,7 +140,6 @@ - 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. @@ -215,6 +201,7 @@ + @@ -225,7 +212,7 @@ - Author of the IDS can provide an identifier to the IDS. Beware: this cannot be enforced/assumed as (global) unique. + Author of the IDS can provide an identifier to the specification. This is intended to be a machine readable identifier. Beware: because of the possibility to combine different 'requirement' elements from several ids files this cannot be enforced/assumed as (global) unique. @@ -242,10 +229,12 @@ - - - - + + + + + + - + \ No newline at end of file