diff --git a/src/ifctester/build/.gitignore b/src/ifctester/build/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/src/ifctester/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/ifctester/build/files/.gitignore b/src/ifctester/build/files/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/src/ifctester/build/files/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/ifctester/ifctester/__init__.py b/src/ifctester/ifctester/__init__.py index e69de29bb2..e070f06934 100644 --- a/src/ifctester/ifctester/__init__.py +++ b/src/ifctester/ifctester/__init__.py @@ -0,0 +1,19 @@ +# IfcTester - IDS based model auditing +# Copyright (C) 2022 Artur Tomczak , Thomas Krijnen , Dion Moult +# +# This file is part of IfcTester. +# +# IfcTester is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcTester is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcTester. If not, see . + +from .ids import open diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py new file mode 100644 index 0000000000..22d65e694b --- /dev/null +++ b/src/ifctester/ifctester/facet.py @@ -0,0 +1,653 @@ +# IfcTester - IDS based model auditing +# Copyright (C) 2021 Artur Tomczak , Thomas Krijnen , Dion Moult +# +# This file is part of IfcTester. +# +# IfcTester is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcTester is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcTester. If not, see . + +import re +import builtins +import ifcopenshell.util.unit +import ifcopenshell.util.element +import ifcopenshell.util.classification +from xmlschema.validators import identities + + +def cast_to_value(from_value, to_value): + try: + target_type = type(to_value).__name__ + if target_type == "int": + # Casting str -> float -> int means that notation like '1e3' is preserved + return int(float(from_value)) + elif target_type == "bool": + if from_value == "TRUE": + return True + elif from_value == "FALSE": + return False + return builtins.__dict__[target_type](from_value) + except ValueError: + pass + + +class Facet: + def __init__(self, *parameters): + self.failed_entities = [] + self.failed_reasons = [] + for i, name in enumerate(self.parameters): + setattr(self, name.replace("@", ""), parameters[i]) + + def asdict(self): + results = {} + for name in self.parameters: + value = getattr(self, name.replace("@", "")) + if value is not None: + results[name] = value if "@" in name else self.to_ids_value(value) + return results + + def parse(self, xml): + for name, value in xml.items(): + name = name.replace("@", "") + if isinstance(value, dict) and "simpleValue" in value.keys(): + setattr(self, name, value["simpleValue"]) + elif isinstance(value, dict) and "restriction" in value.keys(): + setattr(self, name, Restriction().parse(value["restriction"][0])) + # TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]] + else: + setattr(self, name, value) + return self + + def filter(self, ifc_file, elements): + return [e for e in elements if self(e)] + + def to_string(self, clause_type): + if clause_type == "applicability": + templates = self.applicability_templates + elif clause_type == "requirement": + templates = self.requirement_templates + + for template in templates: + for key in self.parameters: + key = key.replace("@", "") + value = getattr(self, key) + key_variable = "{" + key + "}" + if value is not None and key_variable in template: + template = template.replace(key_variable, str(value)) + if "{" not in template: + return template + + def to_ids_value(self, parameter): + if isinstance(parameter, str): + parameter_dict = {"simpleValue": parameter} + elif isinstance(parameter, Restriction): + parameter_dict = {"xs:restriction": [parameter.asdict()]} + elif isinstance(parameter, list): + restrictions = {"@base": "xs:" + parameter[0].base} + for p in parameter: + x = p.asdict() + restrictions[list(x)[1]] = x[list(x)[1]] + parameter_dict = {"xs:restriction": [restrictions]} + else: + raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'") + return parameter_dict + + +class Entity(Facet): + def __init__(self, name="IFCWALL", predefinedType=None, instructions=None): + self.parameters = ["name", "predefinedType", "@instructions"] + self.applicability_templates = [ + "All {name} data of type {predefinedType}", + "All {name} data", + ] + self.requirement_templates = [ + "Shall be {name} data of type {predefinedType}", + "Shall be {name} data", + ] + super().__init__(name, predefinedType, instructions) + + def filter(self, ifc_file, elements): + if isinstance(self.name, str): + results = ifc_file.by_type(self.name, include_subtypes=False) + else: + results = [] + ifc_classes = [t for t in ifc_file.wrapped_data.types() if t.upper() == self.name] + [results.append(ifc_file.by_type(ifc_class, include_subtypes=False)) for ifc_class in ifc_classes] + if self.predefinedType: + return [r for r in results if self(r)] + return results + + def __call__(self, inst, logger=None): + is_pass = inst.is_a().upper() == self.name + reason = None + + if not is_pass: + reason = {"type": "NAME", "actual": inst.is_a().upper()} + + if is_pass and self.predefinedType: + predefined_type = ifcopenshell.util.element.get_predefined_type(inst) + is_pass = predefined_type == self.predefinedType + + if not is_pass: + reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type} + + return EntityResult(is_pass, reason) + + +class Attribute(Facet): + def __init__(self, name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None): + self.parameters = ["name", "value", "@minOccurs", "@maxOccurs", "@instructions"] + self.applicability_templates = [ + "Data where the {name} is {value}", + "Data where the {name} is provided", + ] + self.requirement_templates = [ + "The {name} shall be {value}", + "The {name} shall be provided", + ] + super().__init__(name, value, minOccurs, maxOccurs, instructions) + + def __call__(self, inst, logger=None): + def get_values(element, name): + if isinstance(name, str): + return [getattr(element, name, None)] + return [v for k, v in element.get_info().items() if k == name] + + element_type = ifcopenshell.util.element.get_type(inst) + + if isinstance(self.name, str): + type_value = getattr(element_type, self.name, None) if element_type else None + occurrence_value = getattr(inst, self.name, None) + names = [self.name] + values = [occurrence_value if occurrence_value is not None else type_value] + else: + if element_type: + info = element_type.get_info() + info.update({k: v for k, v in inst.get_info().items() if v is not None}) + else: + info = inst.get_info() + names = [] + values = [] + for k, v in info.items(): + if k == self.name: + names.append(k) + values.append(v) + + is_pass = bool(values) + reason = None + + if not is_pass: + reason = {"type": "NOVALUE"} + + if is_pass: + for i, value in enumerate(values): + if value is None: + is_pass = False + reason = {"type": "FALSEY", "actual": value} + elif value == "": + is_pass = False + reason = {"type": "FALSEY", "actual": value} + elif value == tuple(): + is_pass = False + reason = {"type": "FALSEY", "actual": value} + else: + argument_index = inst.wrapped_data.get_argument_index(names[i]) + try: + attribute_type = inst.attribute_type(argument_index) + if attribute_type == "LOGICAL" and value == "UNKNOWN": + is_pass = False + reason = {"type": "FALSEY", "actual": value} + except: + if names[i] in inst.wrapped_data.get_inverse_attribute_names(): + is_pass = False + reason = {"type": "INVALID"} + if not is_pass: + break + + if is_pass and self.value: + for value in values: + if isinstance(value, ifcopenshell.entity_instance): + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + elif isinstance(self.value, str) and isinstance(value, str): + if value != self.value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + elif isinstance(self.value, str): + cast_value = cast_to_value(self.value, value) + if value != cast_value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + elif value != self.value: + is_pass = False + reason = {"type": "VALUE", "actual": value} + break + + return AttributeResult(is_pass, reason) + + +class Classification(Facet): + def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): + self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] + self.applicability_templates = [ + "Data having a {system} reference of {value}", + "Data classified using {system}", + "Data classified as {value}", + ] + self.requirement_templates = [ + "Shall have a {system} reference of {value}", + "Shall be classified using {system}", + "Shall be classified as {value}", + ] + super().__init__(value, system, uri, minOccurs, maxOccurs, instructions) + + def filter(self, ifc_file, elements): + pass + + def __call__(self, inst, logger=None): + leaf_references = ifcopenshell.util.classification.get_references(inst) + + references = leaf_references.copy() + for leaf_reference in leaf_references: + references.update(ifcopenshell.util.classification.get_inherited_references(leaf_reference)) + + is_pass = bool(references) + reason = None + + if not is_pass: + reason = {"type": "NOVALUE"} + + if is_pass and self.value: + values = [getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references] + is_pass = any([self.value == v for v in values]) + if not is_pass: + reason = {"type": "VALUE", "actual": values} + + if is_pass and self.system: + systems = [ifcopenshell.util.classification.get_classification(r).Name for r in references] + is_pass = any([self.system == s for s in systems]) + if not is_pass: + reason = {"type": "SYSTEM", "actual": systems} + + return ClassificationResult(is_pass, reason) + + +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 __call__(self, inst, logger=None): + if self.entity == "IfcElementAssembly": + is_pass = False + 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 + for rel in getattr(inst, "HasAssignments", []) or []: + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity): + is_pass = True + + return PartOfResult(is_pass, "TODO") + + +class Property(Facet): + def __init__( + self, + propertySet="Property_Set", + name="PropertyName", + value=None, + measure=None, + uri=None, + minOccurs=None, + maxOccurs=None, + instructions=None, + ): + self.parameters = [ + "propertySet", + "name", + "value", + "@measure", + "@uri", + "@minOccurs", + "@maxOccurs", + "@instructions", + ] + self.applicability_templates = [ + "Elements with {name} data of {value} in the dataset {propertySet}", + "Elements with {name} data in the dataset {propertySet}", + ] + self.requirement_templates = [ + "{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) + + def __call__(self, inst, logger=None): + all_psets = ifcopenshell.util.element.get_psets(inst) + + if isinstance(self.propertySet, str): + pset = all_psets.get(self.propertySet, None) + psets = {self.propertySet: pset} if pset else {} + else: + psets = {k: v for k, v in all_psets.items() if k == self.propertySet} + + is_pass = bool(psets) + + if is_pass: + props = {} + for pset_name, pset_props in psets.items(): + props[pset_name] = {} + if isinstance(self.name, str): + prop = pset_props.get(self.name) + if prop: + props[pset_name][self.name] = prop + else: + props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name} + + if not bool(props[pset_name]): + is_pass = False + break + + if self.measure: + pset_entity = inst.wrapped_data.file.by_id(pset_props["id"]) + for prop_entity in pset_entity.HasProperties: + if ( + prop_entity.Name not in props[pset_name].keys() + or not prop_entity.is_a("IfcPropertySingleValue") + or prop_entity.NominalValue is None + ): + continue + + data_type = prop_entity.NominalValue.is_a().replace("Ifc", "").replace("Measure", "") + + if data_type != self.measure: + is_pass = False + break + + unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) + + props[pset_name][prop_entity.Name] = ifcopenshell.util.unit.convert( + prop_entity.NominalValue.wrappedValue, + getattr(unit, "Prefix", None), + unit.Name, + None, + ifcopenshell.util.unit.si_type_names[unit.UnitType], + ) + + if not is_pass: + break + + if self.value: + if any([v != self.value for v in props[pset_name].values()]): + is_pass = False + break + return PropertyResult(is_pass, "todo") + + +class Material(Facet): + def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): + self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] + self.applicability_templates = [ + "All data with a {value} material", + "All data with a material", + ] + self.requirement_templates = [ + "Shall shall have a material of {value}", + "Shall have a material", + ] + super().__init__(value, uri, minOccurs, maxOccurs, instructions) + + def __call__(self, inst, logger=None): + material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True) + + is_pass = material is not None + reason = None + + if not is_pass: + reason = {"type": "NOVALUE"} + + if is_pass and self.value: + if material.is_a("IfcMaterial"): + values = {material.Name, getattr(material, "Category")} + elif material.is_a("IfcMaterialList"): + values = set() + for mat in material.Materials or []: + values.update([mat.Name, getattr(mat, "Category")]) + elif material.is_a("IfcMaterialLayerSet"): + values = {material.LayerSetName} + for item in material.MaterialLayers or []: + values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) + elif material.is_a("IfcMaterialProfileSet"): + values = {material.Name} + for item in material.MaterialProfiles or []: + values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) + elif material.is_a("IfcMaterialConstituentSet"): + values = {material.Name} + for item in material.MaterialConstituents or []: + values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) + + is_pass = False + for value in values: + if value == self.value: + is_pass = True + break + + if not is_pass: + reason = {"type": "VALUE", "actual": values} + + return MaterialResult(is_pass, reason) + + +class Restriction: + def __init__(self, options="", type="pattern", base="string"): + if type in ["enumeration", "pattern", "bounds"]: + self.type = type + self.base = base + self.options = options + if ( + (type == "enumeration" and isinstance(options, list)) + or (type == "bounds" and isinstance(options, dict)) + or (type == "pattern" and isinstance(options, str)) + ): + self.options = options + else: + raise Exception("Options were not properly defined.") + + def parse(self, ids_dict): + if ids_dict: + try: + self.base = ids_dict["@base"][3:] + except KeyError: + self.base = "String" + + for n in ids_dict: + if n == "enumeration": + self.type = "enumeration" + self.options = [] + for x in ids_dict[n]: + self.options.append(x["@value"]) + elif n[-7:] == "clusive": + self.type = "bounds" + self.options = {} + self.options.append({n: ids_dict[n]["@value"]}) + elif n[-5:] == "ength": + self.type = "length" + if n[3:6] == "min": + self.options.append(">=") + elif n[3:6] == "max": + self.options.append("<=") + else: + self.options.append("==") + self.options[-1] += str(ids_dict[n]["@value"]) + elif n == "pattern": + self.type = "pattern" + self.options = ids_dict[n]["@value"] + # TODO add fractionDigits + # TODO add totalDigits + # TODO add whiteSpace + elif n == "@base": + pass + else: + print("Error! Restriction not implemented") + return self + + def asdict(self): + rest_dict = {"@base": "xs:" + self.base} + if self.type == "enumeration": + for option in self.options: + if "xs:enumeration" not in rest_dict: + rest_dict["xs:enumeration"] = [{"@value": option}] + else: + rest_dict["xs:enumeration"].append({"@value": option}) + elif self.type == "bounds": + for option in self.options: + rest_dict["xs:" + option] = [{"@value": str(self.options[option]), "@fixed": False}] + elif self.type == "pattern": + if "xs:pattern" not in rest_dict: + rest_dict["xs:pattern"] = [{"@value": self.options}] + else: + rest_dict["xs:pattern"].append({"@value": self.options}) + return rest_dict + + def __eq__(self, other): + result = False + if self and (other or other == 0): + if self.type == "enumeration" and self.base == "bool": + self.options = [x.lower() for x in self.options] + result = str(other).lower() in self.options + elif self.type == "enumeration": + result = other in [cast_to_value(o, other) for o in self.options] + elif self.type == "bounds": + result = True + for sign in self.options.keys(): + if sign == "minInclusive" and other < self.options[sign]: + result = False + elif sign == "maxInclusive" and other > self.options[sign]: + result = False + elif sign == "minExclusive" and other <= self.options[sign]: + result = False + elif sign == "maxExclusive" and other >= self.options[sign]: + result = False + elif self.type == "length": + for op in self.options: + if eval(str(len(other)) + op): # TODO eval not safe? + result = True + elif self.type == "pattern": + if isinstance(self.options, list): + # TODO handle case with multiple pattern options + translated_pattern = identities.translate_pattern(self.options[0]) + else: + translated_pattern = identities.translate_pattern(self.options) + regex_pattern = re.compile(translated_pattern) + if regex_pattern.fullmatch(other) is not None: + result = True + # TODO add fractionDigits + # TODO add totalDigits + # TODO add whiteSpace + return result + + def __str__(self): + if self.type == "enumeration": + msg = "one of '%s'" % "' or '".join(self.options) + elif self.type == "bounds": + bounds = { + "minInclusive": "larger or equal ", + "maxInclusive": "smaller or equal ", + "minExclusive": "larger than ", + "maxExclusive": "smaller than ", + } + msg = "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options]) + elif self.type == "length": + msg = "with %s letters" % " and ".join(self.options) + elif self.type == "pattern": + msg = "with pattern '%s'" % self.options + # TODO add fractionDigits + # TODO add totalDigits + # TODO add whiteSpace + return msg + + +class Result: + def __init__(self, is_pass, reason=None): + self.is_pass = is_pass + self.reason = reason + + def __bool__(self): + return self.is_pass + + def __str__(self): + return "" if self.is_pass else self.to_string() + + def to_string(self): + return str(self.reason) or "The requirements were not met for some inexplicable reason. Good luck!" + + +class EntityResult(Result): + def to_string(self): + if self.reason["type"] == "NAME": + return f"The entity class \"{self.reason['actual']}\" does not meet the required IFC class" + elif self.reason["type"] == "PREDEFINEDTYPE": + return f"The predefined type \"{str(self.reason['actual'])}\" does not meet the required type" + + +class AttributeResult(Result): + def to_string(self): + if self.reason["type"] == "NOVALUE": + return "The required attribute did not exist" + elif self.reason["type"] == "FALSEY": + return f"The attribute value \"{str(self.reason['actual'])}\" is empty" + elif self.reason["type"] == "INVALID": + return f"An invalid attribute name was specified in the IDS" + elif self.reason["type"] == "VALUE": + return f"The attribute value \"{str(self.reason['actual'])}\" does not match the requirement" + + +class ClassificationResult(Result): + def to_string(self): + if self.reason["type"] == "NOVALUE": + return "The entity has no classification" + elif self.reason["type"] == "VALUE": + return f"The found references \"{str(self.reason['actual'])}\" do not match the requirements" + elif self.reason["type"] == "VALUE": + return f"The references \"{str(self.reason['actual'])}\" do not match the requirements" + elif self.reason["type"] == "system": + return f"The systems \"{str(self.reason['actual'])}\" do not match the requirements" + + +class PartOfResult(Result): + def to_string(self): + return "TODO" + + +class PropertyResult(Result): + def to_string(self): + return "TODO" + + +class MaterialResult(Result): + def to_string(self): + if self.reason["type"] == "NOVALUE": + return "The entity has no material" + elif self.reason["type"] == "VALUE": + return ( + f"The material names and categories of \"{str(self.reason['actual'])}\" does not match the requirement" + ) diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index 8118379599..51fb043a8d 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -17,36 +17,34 @@ # along with IfcTester. If not, see . import os -import re import datetime -import builtins -import ifcopenshell.util.unit -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.classification from xmlschema import XMLSchema from xmlschema import etree_tostring -from xmlschema.validators import identities from xml.etree import ElementTree as ET +from .facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction cwd = os.path.dirname(os.path.realpath(__file__)) -schema = XMLSchema(os.path.join(cwd, "ids.xsd")) +schema = None -def open(filepath): - """Use to open ids.xml files - - :param filepath: ids file path - :type filepath: str - :return: IDS file as a python object - """ - # schema.validate(filepath) +def open(filepath, validate=False): + if validate: + get_schema().validate(filepath) return Ids().parse( - schema.decode(filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}) + get_schema().decode( + filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"} + ) ) +def get_schema(): + global schema + if schema is None: + schema = XMLSchema(os.path.join(cwd, "ids.xsd")) + return schema + + class Ids: def __init__( self, @@ -109,57 +107,17 @@ class Ids: def to_string(self): ns = {"": "http://standards.buildingsmart.org/IDS"} - return etree_tostring(schema.encode(self.asdict()), namespaces=ns) + return etree_tostring(get_schema().encode(self.asdict()), namespaces=ns) def to_xml(self, filepath="output.xml"): ET.register_namespace("", "http://standards.buildingsmart.org/IDS") - ET.ElementTree(schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) - return schema.is_valid(filepath) + ET.ElementTree(get_schema().encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) + return get_schema().is_valid(filepath) def validate(self, ifc_file): for specification in self.specifications: - specification.applicable_entities.clear() - specification.failed_entities = set() - for facet in specification.requirements: - facet.failed_entities.clear() - specification.status = None - - filtered_elements = {} - - for i, specification in enumerate(self.specifications): - if ifc_file.schema not in specification.ifcVersion: - continue - - elements = [] - for facet in specification.applicability: - elements = facet.filter(ifc_file, elements) - - for element in elements: - is_applicable = True - for facet in specification.applicability: - if isinstance(facet, Entity): - continue - if not bool(facet(element)): - is_applicable = False - break - if not is_applicable: - continue - specification.applicable_entities.append(element) - for facet in specification.requirements: - result = facet(element) - if not bool(result): - specification.failed_entities.add(element) - facet.failed_entities.append(element) - facet.failed_reasons.append(str(result)) - - for specification in self.specifications: - specification.status = True - if specification.failed_entities: - specification.status = False - elif specification.minOccurs != 0 and not specification.applicable_entities: - specification.status = False - elif len(specification.applicable_entities) > (specification.maxOccurs or 1): - specification.status = False + specification.reset_status() + specification.validate(ifc_file) class Specification: @@ -231,628 +189,43 @@ class Specification: results.append(facet) return results + def reset_status(self): + self.applicable_entities.clear() + self.failed_entities = set() + for facet in self.requirements: + facet.failed_entities.clear() + self.status = None -class Facet: - def __init__(self, *parameters): - self.failed_entities = [] - self.failed_reasons = [] - for i, name in enumerate(self.parameters): - setattr(self, name.replace("@", ""), parameters[i]) + def validate(self, ifc_file): + if ifc_file.schema not in self.ifcVersion: + return - def asdict(self): - results = {} - for name in self.parameters: - value = getattr(self, name.replace("@", "")) - if value is not None: - results[name] = value if "@" in name else self.to_ids_value(value) - return results + elements = [] + for facet in self.applicability: + elements = facet.filter(ifc_file, elements) - def parse(self, xml): - for name, value in xml.items(): - name = name.replace("@", "") - if isinstance(value, dict) and "simpleValue" in value.keys(): - setattr(self, name, value["simpleValue"]) - elif isinstance(value, dict) and "restriction" in value.keys(): - setattr(self, name, Restriction().parse(value["restriction"][0])) - # TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]] - else: - setattr(self, name, value) - return self - - def filter(self, ifc_file, elements): - return [e for e in elements if self(e)] - - def to_string(self, clause_type): - if clause_type == "applicability": - templates = self.applicability_templates - elif clause_type == "requirement": - templates = self.requirement_templates - - for template in templates: - for key in self.parameters: - key = key.replace("@", "") - value = getattr(self, key) - key_variable = "{" + key + "}" - if value is not None and key_variable in template: - template = template.replace(key_variable, str(value)) - if "{" not in template: - return template - - def to_ids_value(self, parameter): - if isinstance(parameter, str): - parameter_dict = {"simpleValue": parameter} - elif isinstance(parameter, Restriction): - parameter_dict = {"xs:restriction": [parameter.asdict()]} - elif isinstance(parameter, list): - restrictions = {"@base": "xs:" + parameter[0].base} - for p in parameter: - x = p.asdict() - restrictions[list(x)[1]] = x[list(x)[1]] - parameter_dict = {"xs:restriction": [restrictions]} - else: - raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'") - return parameter_dict - - -class Entity(Facet): - def __init__(self, name="IFCWALL", predefinedType=None, instructions=None): - self.parameters = ["name", "predefinedType", "@instructions"] - self.applicability_templates = [ - "All {name} data of type {predefinedType}", - "All {name} data", - ] - self.requirement_templates = [ - "Shall be {name} data of type {predefinedType}", - "Shall be {name} data", - ] - super().__init__(name, predefinedType, instructions) - - def filter(self, ifc_file, elements): - if isinstance(self.name, str): - results = ifc_file.by_type(self.name, include_subtypes=False) - else: - results = [] - ifc_classes = [t for t in ifc_file.wrapped_data.types() if t.upper() == self.name] - [results.append(ifc_file.by_type(ifc_class, include_subtypes=False)) for ifc_class in ifc_classes] - if self.predefinedType: - return [r for r in results if self(r)] - return results - - def __call__(self, inst, logger=None): - is_pass = inst.is_a().upper() == self.name - reason = None - - if not is_pass: - reason = {"type": "NAME", "actual": inst.is_a().upper()} - - if is_pass and self.predefinedType: - predefined_type = ifcopenshell.util.element.get_predefined_type(inst) - is_pass = predefined_type == self.predefinedType - - if not is_pass: - reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type} - - return EntityResult(is_pass, reason) - - -class Attribute(Facet): - def __init__(self, name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None): - self.parameters = ["name", "value", "@minOccurs", "@maxOccurs", "@instructions"] - self.applicability_templates = [ - "Data where the {name} is {value}", - "Data where the {name} is provided", - ] - self.requirement_templates = [ - "The {name} shall be {value}", - "The {name} shall be provided", - ] - super().__init__(name, value, minOccurs, maxOccurs, instructions) - - def __call__(self, inst, logger=None): - def get_values(element, name): - if isinstance(name, str): - return [getattr(element, name, None)] - return [v for k, v in element.get_info().items() if k == name] - - element_type = ifcopenshell.util.element.get_type(inst) - - if isinstance(self.name, str): - type_value = getattr(element_type, self.name, None) if element_type else None - occurrence_value = getattr(inst, self.name, None) - names = [self.name] - values = [occurrence_value if occurrence_value is not None else type_value] - else: - if element_type: - info = element_type.get_info() - info.update({k: v for k, v in inst.get_info().items() if v is not None}) - else: - info = inst.get_info() - names = [] - values = [] - for k, v in info.items(): - if k == self.name: - names.append(k) - values.append(v) - - is_pass = bool(values) - reason = None - - if not is_pass: - reason = {"type": "NOVALUE"} - - if is_pass: - for i, value in enumerate(values): - if value is None: - is_pass = False - reason = {"type": "FALSEY", "actual": value} - elif value == "": - is_pass = False - reason = {"type": "FALSEY", "actual": value} - elif value == tuple(): - is_pass = False - reason = {"type": "FALSEY", "actual": value} - else: - argument_index = inst.wrapped_data.get_argument_index(names[i]) - try: - attribute_type = inst.attribute_type(argument_index) - if attribute_type == "LOGICAL" and value == "UNKNOWN": - is_pass = False - reason = {"type": "FALSEY", "actual": value} - except: - if names[i] in inst.wrapped_data.get_inverse_attribute_names(): - is_pass = False - reason = {"type": "INVALID"} - if not is_pass: + for element in elements: + is_applicable = True + for facet in self.applicability: + if isinstance(facet, Entity): + continue + if not bool(facet(element)): + is_applicable = False break + if not is_applicable: + continue + self.applicable_entities.append(element) + for facet in self.requirements: + result = facet(element) + if not bool(result): + self.failed_entities.add(element) + facet.failed_entities.append(element) + facet.failed_reasons.append(str(result)) - if is_pass and self.value: - for value in values: - if isinstance(value, ifcopenshell.entity_instance): - is_pass = False - reason = {"type": "VALUE", "actual": value} - break - elif isinstance(self.value, str) and isinstance(value, str): - if value != self.value: - is_pass = False - reason = {"type": "VALUE", "actual": value} - break - elif isinstance(self.value, str): - cast_value = cast_to_value(self.value, value) - if value != cast_value: - is_pass = False - reason = {"type": "VALUE", "actual": value} - break - elif value != self.value: - is_pass = False - reason = {"type": "VALUE", "actual": value} - break - - return AttributeResult(is_pass, reason) - - -class Classification(Facet): - def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): - self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] - self.applicability_templates = [ - "Data having a {system} reference of {value}", - "Data classified using {system}", - "Data classified as {value}", - ] - self.requirement_templates = [ - "Shall have a {system} reference of {value}", - "Shall be classified using {system}", - "Shall be classified as {value}", - ] - super().__init__(value, system, uri, minOccurs, maxOccurs, instructions) - - def __call__(self, inst, logger=None): - leaf_references = ifcopenshell.util.classification.get_references(inst) - - references = leaf_references.copy() - for leaf_reference in leaf_references: - references.update(ifcopenshell.util.classification.get_inherited_references(leaf_reference)) - - is_pass = bool(references) - reason = None - - if not is_pass: - reason = {"type": "NOVALUE"} - - if is_pass and self.value: - values = [getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references] - is_pass = any([self.value == v for v in values]) - if not is_pass: - reason = {"type": "VALUE", "actual": values} - - if is_pass and self.system: - systems = [ifcopenshell.util.classification.get_classification(r).Name for r in references] - is_pass = any([self.system == s for s in systems]) - if not is_pass: - reason = {"type": "SYSTEM", "actual": systems} - - return ClassificationResult(is_pass, reason) - - -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 __call__(self, inst, logger=None): - if self.entity == "IfcElementAssembly": - is_pass = False - 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 - for rel in getattr(inst, "HasAssignments", []) or []: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity): - is_pass = True - - return PartOfResult(is_pass, "TODO") - - -class Property(Facet): - def __init__( - self, - propertySet="Property_Set", - name="PropertyName", - value=None, - measure=None, - uri=None, - minOccurs=None, - maxOccurs=None, - instructions=None, - ): - self.parameters = [ - "propertySet", - "name", - "value", - "@measure", - "@uri", - "@minOccurs", - "@maxOccurs", - "@instructions", - ] - self.applicability_templates = [ - "Elements with {name} data of {value} in the dataset {propertySet}", - "Elements with {name} data in the dataset {propertySet}", - ] - self.requirement_templates = [ - "{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) - - def __call__(self, inst, logger=None): - all_psets = ifcopenshell.util.element.get_psets(inst) - - if isinstance(self.propertySet, str): - pset = all_psets.get(self.propertySet, None) - psets = {self.propertySet: pset} if pset else {} - else: - psets = {k: v for k, v in all_psets.items() if k == self.propertySet} - - is_pass = bool(psets) - - if is_pass: - props = {} - for pset_name, pset_props in psets.items(): - props[pset_name] = {} - if isinstance(self.name, str): - prop = pset_props.get(self.name) - if prop: - props[pset_name][self.name] = prop - else: - props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name} - - if not bool(props[pset_name]): - is_pass = False - break - - if self.measure: - pset_entity = inst.wrapped_data.file.by_id(pset_props["id"]) - for prop_entity in pset_entity.HasProperties: - if ( - prop_entity.Name not in props[pset_name].keys() - or not prop_entity.is_a("IfcPropertySingleValue") - or prop_entity.NominalValue is None - ): - continue - - data_type = prop_entity.NominalValue.is_a().replace("Ifc", "").replace("Measure", "") - - if data_type != self.measure: - is_pass = False - break - - unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) - - props[pset_name][prop_entity.Name] = ifcopenshell.util.unit.convert( - prop_entity.NominalValue.wrappedValue, - getattr(unit, "Prefix", None), - unit.Name, - None, - ifcopenshell.util.unit.si_type_names[unit.UnitType], - ) - - if not is_pass: - break - - if self.value: - if any([v != self.value for v in props[pset_name].values()]): - is_pass = False - break - return PropertyResult(is_pass, "todo") - - -class Material(Facet): - def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): - self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] - self.applicability_templates = [ - "All data with a {value} material", - "All data with a material", - ] - self.requirement_templates = [ - "Shall shall have a material of {value}", - "Shall have a material", - ] - super().__init__(value, uri, minOccurs, maxOccurs, instructions) - - def __call__(self, inst, logger=None): - material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True) - - is_pass = material is not None - reason = None - - if not is_pass: - reason = {"type": "NOVALUE"} - - if is_pass and self.value: - if material.is_a("IfcMaterial"): - values = {material.Name, getattr(material, "Category")} - elif material.is_a("IfcMaterialList"): - values = set() - for mat in material.Materials or []: - values.update([mat.Name, getattr(mat, "Category")]) - elif material.is_a("IfcMaterialLayerSet"): - values = {material.LayerSetName} - for item in material.MaterialLayers or []: - values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) - elif material.is_a("IfcMaterialProfileSet"): - values = {material.Name} - for item in material.MaterialProfiles or []: - values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) - elif material.is_a("IfcMaterialConstituentSet"): - values = {material.Name} - for item in material.MaterialConstituents or []: - values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")]) - - is_pass = False - for value in values: - if value == self.value: - is_pass = True - break - - if not is_pass: - reason = {"type": "VALUE", "actual": values} - - return MaterialResult(is_pass, reason) - - -def cast_to_value(from_value, to_value): - try: - target_type = type(to_value).__name__ - if target_type == "int": - # Casting str -> float -> int means that notation like '1e3' is preserved - return int(float(from_value)) - elif target_type == "bool": - if from_value == "TRUE": - return True - elif from_value == "FALSE": - return False - return builtins.__dict__[target_type](from_value) - except ValueError: - pass - - -class Restriction: - def __init__(self, options="", type="pattern", base="string"): - if type in ["enumeration", "pattern", "bounds"]: - self.type = type - self.base = base - self.options = options - if ( - (type == "enumeration" and isinstance(options, list)) - or (type == "bounds" and isinstance(options, dict)) - or (type == "pattern" and isinstance(options, str)) - ): - self.options = options - else: - raise Exception("Options were not properly defined.") - - def parse(self, ids_dict): - if ids_dict: - try: - self.base = ids_dict["@base"][3:] - except KeyError: - self.base = "String" - - for n in ids_dict: - if n == "enumeration": - self.type = "enumeration" - self.options = [] - for x in ids_dict[n]: - self.options.append(x["@value"]) - elif n[-7:] == "clusive": - self.type = "bounds" - self.options = {} - self.options.append({n: ids_dict[n]["@value"]}) - elif n[-5:] == "ength": - self.type = "length" - if n[3:6] == "min": - self.options.append(">=") - elif n[3:6] == "max": - self.options.append("<=") - else: - self.options.append("==") - self.options[-1] += str(ids_dict[n]["@value"]) - elif n == "pattern": - self.type = "pattern" - self.options = ids_dict[n]["@value"] - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - elif n == "@base": - pass - else: - print("Error! Restriction not implemented") - return self - - def asdict(self): - rest_dict = {"@base": "xs:" + self.base} - if self.type == "enumeration": - for option in self.options: - if "xs:enumeration" not in rest_dict: - rest_dict["xs:enumeration"] = [{"@value": option}] - else: - rest_dict["xs:enumeration"].append({"@value": option}) - elif self.type == "bounds": - for option in self.options: - rest_dict["xs:" + option] = [{"@value": str(self.options[option]), "@fixed": False}] - elif self.type == "pattern": - if "xs:pattern" not in rest_dict: - rest_dict["xs:pattern"] = [{"@value": self.options}] - else: - rest_dict["xs:pattern"].append({"@value": self.options}) - return rest_dict - - def __eq__(self, other): - result = False - if self and (other or other == 0): - if self.type == "enumeration" and self.base == "bool": - self.options = [x.lower() for x in self.options] - result = str(other).lower() in self.options - elif self.type == "enumeration": - result = other in [cast_to_value(o, other) for o in self.options] - elif self.type == "bounds": - result = True - for sign in self.options.keys(): - if sign == "minInclusive" and other < self.options[sign]: - result = False - elif sign == "maxInclusive" and other > self.options[sign]: - result = False - elif sign == "minExclusive" and other <= self.options[sign]: - result = False - elif sign == "maxExclusive" and other >= self.options[sign]: - result = False - elif self.type == "length": - for op in self.options: - if eval(str(len(other)) + op): # TODO eval not safe? - result = True - elif self.type == "pattern": - if isinstance(self.options, list): - # TODO handle case with multiple pattern options - translated_pattern = identities.translate_pattern(self.options[0]) - else: - translated_pattern = identities.translate_pattern(self.options) - regex_pattern = re.compile(translated_pattern) - if regex_pattern.fullmatch(other) is not None: - result = True - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - return result - - def __str__(self): - if self.type == "enumeration": - msg = "one of '%s'" % "' or '".join(self.options) - elif self.type == "bounds": - bounds = { - "minInclusive": "larger or equal ", - "maxInclusive": "smaller or equal ", - "minExclusive": "larger than ", - "maxExclusive": "smaller than ", - } - msg = "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options]) - elif self.type == "length": - msg = "with %s letters" % " and ".join(self.options) - elif self.type == "pattern": - msg = "with pattern '%s'" % self.options - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - return msg - - -class Result: - def __init__(self, is_pass, reason=None): - self.is_pass = is_pass - self.reason = reason - - def __bool__(self): - return self.is_pass - - def __str__(self): - return "" if self.is_pass else self.to_string() - - def to_string(self): - return str(self.reason) or "The requirements were not met for some inexplicable reason. Good luck!" - - -class EntityResult(Result): - def to_string(self): - if self.reason["type"] == "NAME": - return f"The entity class \"{self.reason['actual']}\" does not meet the required IFC class" - elif self.reason["type"] == "PREDEFINEDTYPE": - return f"The predefined type \"{str(self.reason['actual'])}\" does not meet the required type" - - -class AttributeResult(Result): - def to_string(self): - if self.reason["type"] == "NOVALUE": - return "The required attribute did not exist" - elif self.reason["type"] == "FALSEY": - return f"The attribute value \"{str(self.reason['actual'])}\" is empty" - elif self.reason["type"] == "INVALID": - return f"An invalid attribute name was specified in the IDS" - elif self.reason["type"] == "VALUE": - return f"The attribute value \"{str(self.reason['actual'])}\" does not match the requirement" - - -class ClassificationResult(Result): - def to_string(self): - if self.reason["type"] == "NOVALUE": - return "The entity has no classification" - elif self.reason["type"] == "VALUE": - return f"The found references \"{str(self.reason['actual'])}\" do not match the requirements" - elif self.reason["type"] == "VALUE": - return f"The references \"{str(self.reason['actual'])}\" do not match the requirements" - elif self.reason["type"] == "system": - return f"The systems \"{str(self.reason['actual'])}\" do not match the requirements" - - -class PartOfResult(Result): - def to_string(self): - return "TODO" - - -class PropertyResult(Result): - def to_string(self): - return "TODO" - - -class MaterialResult(Result): - def to_string(self): - if self.reason["type"] == "NOVALUE": - return "The entity has no material" - elif self.reason["type"] == "VALUE": - return ( - f"The material names and categories of \"{str(self.reason['actual'])}\" does not match the requirement" - ) + self.status = True + if self.failed_entities: + self.status = False + elif self.minOccurs != 0 and not self.applicable_entities: + self.status = False + elif len(self.applicable_entities) > (self.maxOccurs or 1): + self.status = False diff --git a/src/ifctester/test/ids_doc_generator.py b/src/ifctester/test/ids_doc_generator.py index 1b1f07ec41..83131dcbac 100644 --- a/src/ifctester/test/ids_doc_generator.py +++ b/src/ifctester/test/ids_doc_generator.py @@ -1,30 +1,30 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Thomas Krijnen +# IfcTester - IDS based model auditing +# Copyright (C) 2021-2022 Thomas Krijnen , Dion Moult # -# This file is part of IfcOpenShell. +# This file is part of IfcTester. # -# IfcOpenShell is free software: you can redistribute it and/or modify +# IfcTester is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# IfcOpenShell is distributed in the hope that it will be useful, +# IfcTester is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along with IfcOpenShell. If not, see . +# along with IfcTester. If not, see . import os import re -import unittest +import pytest import functools -import itertools import ifcopenshell -import test_ids +import test_facet from xml.dom.minidom import parseString -from ifcopenshell import ids, template, validate +from ifctester import ids +from ifcopenshell import validate outdir = "build" @@ -42,8 +42,7 @@ class DocGenerator: f = inst.wrapped_data.file - # Validate the file created and loop over the issues, fixing them - # one by one. + # Validate the file created and loop over the issues, fixing them one by one. l = validate.json_logger() validate.validate(f, l) for issue in l.statements: @@ -63,18 +62,18 @@ class DocGenerator: basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower()) # Write IFC to disk - f.write(os.path.join(outdir, f"{basename}.ifc")) + f.write(os.path.join(outdir, "files", f"{basename}.ifc")) # Create an IDS with the applicability selecting exactly # the entity type passed to us in `inst`. - specs = ids.ids(title=name) - spec = ids.specification(name=name) - spec.add_applicability(ids.entity.create(name=inst.is_a())) - spec.add_requirement(facet) + specs = ids.Ids(title=name) + spec = ids.Specification(name=name) + spec.applicability.append(ids.Entity(name=inst.is_a())) + spec.requirements.append(facet) specs.specifications.append(spec) # Write IDS to disk - with open(os.path.join(outdir, f"{basename}.ids"), "w", encoding="utf-8") as ids_file: + with open(os.path.join(outdir, "files", f"{basename}.ids"), "w", encoding="utf-8") as ids_file: ids_file.write(specs.to_string()) xml_text = "\n".join( @@ -97,13 +96,12 @@ class DocGenerator: self.facet = facet -test_ids.run = DocGenerator() -test_ids.set_facet = test_ids.run.set_facet +test_facet.run = DocGenerator() +test_facet.set_facet = test_facet.run.set_facet -suite = unittest.TestLoader().discover(".", pattern="test_ids.py") -result = unittest.TextTestRunner(verbosity=2).run(suite) +pytest.main(["-p", "no:pytest-blender"]) -for facet, testcases in test_ids.run.testcases.items(): +for facet, testcases in test_facet.run.testcases.items(): with open(os.path.join(outdir, f"testcases-{facet}.md"), "w") as f: write = functools.partial(print, file=f) write(f"# {facet.capitalize()} testcases") @@ -124,6 +122,6 @@ for facet, testcases in test_ids.run.testcases.items(): write("~~~") write() write( - f"[Sample IDS]({testcase['basename']}.ids) - [Sample IFC: {testcase['id']}]({testcase['basename']}.ifc)" + f"[Sample IDS](files/{testcase['basename']}.ids) - [Sample IFC: {testcase['id']}](files/{testcase['basename']}.ifc)" ) write() diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py new file mode 100644 index 0000000000..9980eb685b --- /dev/null +++ b/src/ifctester/test/test_facet.py @@ -0,0 +1,1206 @@ +# IfcTester - IDS based model auditing +# Copyright (C) 2021-2022 Thomas Krijnen , Dion Moult +# +# This file is part of IfcTester. +# +# IfcTester is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcTester is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcTester. If not, see . + + +import ifcopenshell +import ifcopenshell.api +from ifctester.facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction + + +def set_facet(facet): + pass + + +def run(name, *, facet, inst, expected): + assert bool(facet(inst)) is expected + + +class TestEntity: + def test_creating_an_entity_facet(self): + facet = Entity(name="IfcName") + assert facet.asdict() == {"name": {"simpleValue": "IfcName"}} + facet = Entity(name="IfcName", predefinedType="predefinedType", instructions="instructions") + assert facet.asdict() == { + "name": {"simpleValue": "IfcName"}, + "predefinedType": {"simpleValue": "predefinedType"}, + "@instructions": "instructions", + } + + def test_filtering_using_an_entity_facet(self): + set_facet("entity") + + ifc = ifcopenshell.file() + facet = Entity(name="IFCRABBIT") + run("Invalid entities always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) + + ifc = ifcopenshell.file() + facet = Entity(name="IFCWALL") + run("A matching entity should pass", facet=facet, inst=ifc.createIfcWall(), expected=True) + ifc = ifcopenshell.file() + run( + "An matching entity should pass regardless of predefined type", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), + expected=True, + ) + ifc = ifcopenshell.file() + run( + "An entity not matching the specified class should fail", + facet=facet, + inst=ifc.createIfcSlab(), + expected=False, + ) + # TODO: Some votes to prefer inheritance (For: Moult, Evandro, Artur) + # Possible argument: CAD tools don't understand IFC + ifc = ifcopenshell.file() + run( + "Subclasses are not considered as matching", + facet=facet, + inst=ifc.createIfcWallStandardCase(), + expected=False, + ) + + # TODO But in that case why are the enumerations for things like partOf using the IFC capitalisation? + facet = Entity(name="IfcWall") + ifc = ifcopenshell.file() + run( + "Entities must be specified as uppercase strings", + facet=facet, + inst=ifc.createIfcWall(), + expected=False, + ) + + facet = Entity(name="IFCWALL", predefinedType="SOLIDWALL") + ifc = ifcopenshell.file() + run( + "A matching predefined type should pass", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), + expected=True, + ) + ifc = ifcopenshell.file() + run( + "A null predefined type should always fail a specified predefined types", + facet=facet, + inst=ifc.createIfcWall(), + expected=False, + ) + ifc = ifcopenshell.file() + run( + "An entity not matching a specified predefined type will fail", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="PARTITIONING"), + expected=False, + ) + + facet = Entity(name="IFCWALL", predefinedType="solidwall") + ifc = ifcopenshell.file() + run( + "A predefined type from an enumeration must be uppercase", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), + expected=False, + ) + + facet = Entity(name="IFCWALL", predefinedType="WALDO") + ifc = ifcopenshell.file() + run( + "A predefined type may specify a user-defined object type", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"), + expected=True, + ) + + facet = Entity(name="IFCWALL", predefinedType="WALDO") + ifc = ifcopenshell.file() + run( + "User-defined types are checked case sensitively", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="waldo"), + expected=False, + ) + + facet = Entity(name="IFCWALLTYPE", predefinedType="WALDO") + ifc = ifcopenshell.file() + run( + "A predefined type may specify a user-defined element type", + facet=facet, + inst=ifc.createIfcWallType(PredefinedType="USERDEFINED", ElementType="WALDO"), + expected=True, + ) + + facet = Entity(name="IFCTASKTYPE", predefinedType="TASKY") + ifc = ifcopenshell.file() + run( + "A predefined type may specify a user-defined process type", + facet=facet, + inst=ifc.createIfcTaskType(PredefinedType="USERDEFINED", ProcessType="TASKY"), + expected=True, + ) + + facet = Entity(name="IFCWALL", predefinedType="USERDEFINED") + ifc = ifcopenshell.file() + run( + "A predefined type must always specify a meaningful type, not USERDEFINED itself", + facet=facet, + inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"), + expected=False, + ) + + ifc = ifcopenshell.file() + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="X") + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + facet = Entity(name="IFCWALL", predefinedType="X") + run("Inherited predefined types should pass", facet=facet, inst=wall, expected=True) + + ifc = ifcopenshell.file() + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="X") + wall_type = ifcopenshell.api.run( + "root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="NOTDEFINED" + ) + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + facet = Entity(name="IFCWALL", predefinedType="X") + run("Overridden predefined types should pass", facet=facet, inst=wall, expected=True) + + restriction = Restriction(options=["IFCWALL", "IFCSLAB"], type="enumeration") + facet = Entity(name=restriction) + ifc = ifcopenshell.file() + run("Entities can be specified as an enumeration 1/3", facet=facet, inst=ifc.createIfcWall(), expected=True) + ifc = ifcopenshell.file() + run("Entities can be specified as an enumeration 2/3", facet=facet, inst=ifc.createIfcSlab(), expected=True) + ifc = ifcopenshell.file() + run("Entities can be specified as an enumeration 3/3", facet=facet, inst=ifc.createIfcBeam(), expected=False) + + restriction = Restriction(options="IFC.*TYPE", type="pattern") + facet = Entity(name=restriction) + ifc = ifcopenshell.file() + run( + "Entities can be specified as a XSD regex pattern 1/2", + facet=facet, + inst=ifc.createIfcWall(), + expected=False, + ) + ifc = ifcopenshell.file() + run( + "Entities can be specified as a XSD regex pattern 2/2", + facet=facet, + inst=ifc.createIfcWallType(), + expected=True, + ) + + restriction = Restriction(options="FOO.*", type="pattern") + facet = Entity(name="IFCWALL", predefinedType=restriction) + ifc = ifcopenshell.file() + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAR") + run("Restrictions an be specified for the predefined type 1/3", facet=facet, inst=wall, expected=True) + ifc = ifcopenshell.file() + wall2 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAZ") + run("Restrictions an be specified for the predefined type 2/3", facet=facet, inst=wall2, expected=True) + ifc = ifcopenshell.file() + wall3 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="BAZFOO") + run("Restrictions an be specified for the predefined type 3/3", facet=facet, inst=wall3, expected=False) + + +class TestAttribute: + def test_creating_an_attribute_facet(self): + attribute = Attribute(name="name") + assert attribute.asdict() == {"name": {"simpleValue": "name"}} + attribute = Attribute(name="name", value="value") + assert attribute.asdict() == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}} + attribute = Attribute( + name="name", value="value", minOccurs="0", maxOccurs="unbounded", instructions="instructions" + ) + assert attribute.asdict() == { + "name": {"simpleValue": "name"}, + "value": {"simpleValue": "value"}, + "@minOccurs": "0", + "@maxOccurs": "unbounded", + "@instructions": "instructions", + } + + def test_filtering_using_an_attribute_facet(self): + set_facet("attribute") + + ifc = ifcopenshell.file() + facet = Attribute(name="Foobar") + run("Invalid attribute names always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) + + ifc = ifcopenshell.file() + facet = Attribute(name="Name") + run( + "Attributes with a string value should pass", + facet=facet, + inst=ifc.createIfcWall(Name="Foobar"), + expected=True, + ) + ifc = ifcopenshell.file() + run("Attributes with null values always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) + # The logic is that unfortunately most BIM users cannot differentiate between the two. + ifc = ifcopenshell.file() + run("Attributes with empty strings always fail", facet=facet, inst=ifc.createIfcWall(Name=""), expected=False) + + facet = Attribute(name="CountValue") + ifc = ifcopenshell.file() + run( + "Attributes with a zero number have meaning and should pass", + facet=facet, + inst=ifc.createIfcQuantityCount(Name="Foobar", CountValue=0), + expected=True, + ) + + facet = Attribute(name="IsCritical") + ifc = ifcopenshell.file() + element = ifc.createIfcTaskTime(IsCritical=True) + run("Attributes with a boolean true should pass", facet=facet, inst=element, expected=True) + element.IsCritical = False + run("Attributes with a boolean false should pass", facet=facet, inst=element, expected=True) + + facet = Attribute(name="RelatingPriorities") + ifc = ifcopenshell.file() + run( + "Attributes with an empty list always fail", + facet=facet, + inst=ifc.createIfcRelConnectsPathElements( + RelatingElement=ifc.createIfcWall(), + RelatedElement=ifc.createIfcWall(), + RelatingPriorities=[], + RelatedPriorities=[], + RelatedConnectionType="ATSTART", + RelatingConnectionType="ATEND", + ), + expected=False, + ) + + facet = Attribute(name="LayerStyles") + ifc = ifcopenshell.file() + item = ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]) + layer = ifc.createIfcPresentationLayerWithStyle("Foo", None, [item], None, True, False, False, []) + run("Attributes with an empty set always fail", facet=facet, inst=layer, expected=False) + + layer.LayerOn = "UNKNOWN" + facet = Attribute(name="LayerOn") + run("Attributes with a logical unknown always fail", facet=facet, inst=layer, expected=False) + + facet = Attribute(name="ScheduleDuration") + ifc = ifcopenshell.file() + element = ifc.createIfcTaskTime(ScheduleDuration="P0D") + run("Attributes with a zero duration should pass", facet=facet, inst=element, expected=True) + + facet = Attribute(name="TaskTime") + ifc = ifcopenshell.file() + element = ifc.createIfcTask(IsMilestone=True, TaskTime=ifc.createIfcTaskTime()) + run("Attributes referencing an object should pass", facet=facet, inst=element, expected=True) + + facet = Attribute(name="DiffuseColour") + ifc = ifcopenshell.file() + rgb = ifc.createIfcColourRgb(None, 1, 1, 1) + run( + "Attributes with a select referencing an object should pass", + facet=facet, + inst=ifc.createIfcSurfaceStyleRendering( + SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcColourRgb(None, 1, 1, 1) + ), + expected=True, + ) + + ifc = ifcopenshell.file() + rgb = ifc.createIfcColourRgb(None, 1, 1, 1) + run( + "Attributes with a select referencing a primitive should pass", + facet=facet, + inst=ifc.createIfcSurfaceStyleRendering( + SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcNormalisedRatioMeasure(0.5) + ), + expected=True, + ) + + ifc = ifcopenshell.file() + facet = Attribute(name="EngagedIn") + person = ifc.createIfcPerson() + organisation = ifc.createIfcOrganization(Name="Foo") + ifc.createIfcPersonAndOrganization(ThePerson=person, TheOrganization=organisation) + run("Inverse attributes cannot be checked and always fail", facet=facet, inst=person, expected=False) + + ifc = ifcopenshell.file() + facet = Attribute(name="Dim") + run( + "Derived attributes cannot be checked and always fail", + facet=facet, + inst=ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]), + expected=False, + ) + + facet = Attribute(name="Name", value="Foobar") + ifc = ifcopenshell.file() + run( + "Attributes should check strings case sensitively 1/2", + facet=facet, + inst=ifc.createIfcWall(Name="Foobar"), + expected=True, + ) + ifc = ifcopenshell.file() + run( + "Attributes should check strings case sensitively 2/2", + facet=facet, + inst=ifc.createIfcWall(Name="foobar"), + expected=False, + ) + + ifc = ifcopenshell.file() + facet = Attribute(name="Name", value="♫") + run( + "Non-ascii characters are treated without encoding", + facet=facet, + inst=ifc.createIfcWall(Name="♫"), + expected=True, + ) + + facet = Attribute(name="TaskTime", value="Foobar") + ifc = ifcopenshell.file() + run( + "Value checks always fail for objects", + facet=facet, + inst=ifc.createIfcTask(IsMilestone=False, TaskTime=ifc.createIfcTaskTime()), + expected=False, + ) + + facet = Attribute(name="DiffuseColour", value="Foobar") + ifc = ifcopenshell.file() + rgb = ifc.createIfcColourRgb(None, 1, 1, 1) + run( + "Value checks always fail for selects", + facet=facet, + inst=ifc.createIfcSurfaceStyleRendering( + SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcNormalisedRatioMeasure(0.5) + ), + expected=False, + ) + + facet = Attribute(name="Coordinates", value="Foobar") + ifc = ifcopenshell.file() + run( + "Value checks always fail for lists", + facet=facet, + inst=ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]), + expected=False, + ) + + # TODO continue from here + + global_id = ifcopenshell.guid.new() + facet = Attribute(name="GlobalId", value=global_id) + ifc = ifcopenshell.file() + run( + "GlobalIds are treated as strings and not expanded", + facet=facet, + inst=ifc.createIfcWall(GlobalId=global_id), + expected=True, + ) + + # 255 characters + identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345" + facet = Attribute(name="Identification", value=identifier + "_extra_characters") + ifc = ifcopenshell.file() + run( + "IDS does not handle string truncation such as for identifiers", + facet=facet, + inst=ifc.createIfcPerson(Identification=identifier), + expected=False, + ) + + facet = Attribute(name="RefractionIndex", value="42") + ifc = ifcopenshell.file() + run( + "Numeric values are checked using type casting 1/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), + expected=True, + ) + facet = Attribute(name="RefractionIndex", value="42.") + ifc = ifcopenshell.file() + run( + "Numeric values are checked using type casting 2/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0), + expected=True, + ) + facet = Attribute(name="RefractionIndex", value="42.0") + ifc = ifcopenshell.file() + run( + "Numeric values are checked using type casting 3/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0), + expected=True, + ) + facet = Attribute(name="RefractionIndex", value="42") + ifc = ifcopenshell.file() + run( + "Numeric values are checked using type casting 4/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.3), + expected=False, + ) + facet = Attribute(name="RefractionIndex", value="42,3") + ifc = ifcopenshell.file() + run( + "Only specifically formatted numbers are allowed 1/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.3), + expected=False, + ) + facet = Attribute(name="RefractionIndex", value="123,4.5") + ifc = ifcopenshell.file() + run( + "Only specifically formatted numbers are allowed 2/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), + expected=False, + ) + facet = Attribute(name="RefractionIndex", value="1.2345e3") + ifc = ifcopenshell.file() + run( + "Only specifically formatted numbers are allowed 3/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), + expected=True, + ) + facet = Attribute(name="RefractionIndex", value="1.2345E3") + ifc = ifcopenshell.file() + run( + "Only specifically formatted numbers are allowed 4/4", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), + expected=True, + ) + + facet = Attribute(name="NumberOfRisers", value="42") + ifc = ifcopenshell.file() + run( + "Integers follow the same rules as numbers", + facet=facet, + inst=ifc.createIfcStairFlight(NumberOfRisers=42), + expected=True, + ) + facet = Attribute(name="NumberOfRisers", value="42.0") + ifc = ifcopenshell.file() + run( + "Integers follow the same rules as numbers 2/2", + facet=facet, + inst=ifc.createIfcStairFlight(NumberOfRisers=42), + expected=True, + ) + + facet = Attribute(name="NumberOfRisers", value="42.3") + ifc = ifcopenshell.file() + run( + "Integers are always floored when cast 1/2", + facet=facet, + inst=ifc.createIfcStairFlight(NumberOfRisers=42), + expected=True, + ) + facet = Attribute(name="NumberOfRisers", value="42.7") + ifc = ifcopenshell.file() + run( + "Integers are always floored when cast 2/2", + facet=facet, + inst=ifc.createIfcStairFlight(NumberOfRisers=42), + expected=True, + ) + + facet = Attribute(name="NumberOfRisers", value="42.7") + ifc = ifcopenshell.file() + run( + "Integers are always floored when cast 2/2", + facet=facet, + inst=ifc.createIfcStairFlight(NumberOfRisers=42), + expected=True, + ) + + facet = Attribute(name="IsMilestone", value="TRUE") + ifc = ifcopenshell.file() + element = ifc.createIfcTask(IsMilestone=False) + run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False) + facet = Attribute(name="IsMilestone", value="FALSE") + run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True) + facet = Attribute(name="IsMilestone", value="False") + run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=False) + + facet = Attribute(name="EditionDate", value="2022-01-01") + ifc = ifcopenshell.file() + run( + "Dates are treated as strings 1/2", + facet=facet, + inst=ifc.createIfcClassification(Name="Name", EditionDate="2022-01-01"), + expected=True, + ) + ifc = ifcopenshell.file() + run( + "Dates are treated as strings 1/2", + facet=facet, + inst=ifc.createIfcClassification(Name="Name", EditionDate="2022-01-01+00:00"), + expected=False, + ) + + facet = Attribute(name="ScheduleDuration", value="PT16H") + ifc = ifcopenshell.file() + run( + "Durations are treated as strings 1/2", + facet=facet, + inst=ifc.createIfcClassification(Name="Name", EditionDate="PT16H"), + expected=False, + ) + ifc = ifcopenshell.file() + run( + "Durations are treated as strings 2/2", + facet=facet, + inst=ifc.createIfcClassification(Name="Name", EditionDate="P2D"), + expected=False, + ) + + restriction = Restriction(options=".*Name.*", type="pattern") + facet = Attribute(name=restriction) + ifc = ifcopenshell.file() + run( + "Name restrictions may be used 1/4", + facet=facet, + inst=ifc.createIfcMaterialLayerSet( + MaterialLayers=[ifc.createIfcMaterialLayer(LayerThickness=1)], LayerSetName="Foo" + ), + expected=True, + ) + ifc = ifcopenshell.file() + run( + "Name restrictions may be used 2/4", + facet=facet, + inst=ifc.createIfcMaterialConstituentSet(Name="Foo"), + expected=True, + ) + + restriction = Restriction(options=["Name", "Description"], type="enumeration") + facet = Attribute(name=restriction) + ifc = ifcopenshell.file() + run("Name restrictions may be used 3/4", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=False) + ifc = ifcopenshell.file() + run( + "Name restrictions may be used 4/4", + facet=facet, + inst=ifc.createIfcWall(Name="Foo", Description="Bar"), + expected=True, + ) + + restriction = Restriction(options=["Foo", "Bar"], type="enumeration") + facet = Attribute(name="Name", value=restriction) + ifc = ifcopenshell.file() + run("Value restrictions may be used 1/3", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=True) + ifc = ifcopenshell.file() + run("Value restrictions may be used 2/3", facet=facet, inst=ifc.createIfcWall(Name="Bar"), expected=True) + ifc = ifcopenshell.file() + run("Value restrictions may be used 3/3", facet=facet, inst=ifc.createIfcWall(Name="Foobar"), expected=False) + + ifc = ifcopenshell.file() + 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) + wall_type.Description = "Foobar" + run("Attributes are not inherited by the occurrence", facet=facet, inst=wall, expected=False) + + restriction = Restriction(options=["42"], type="enumeration", base="string") + facet = Attribute(name="RefractionIndex", value=restriction) + ifc = ifcopenshell.file() + run( + "Typecast checking may also occur within enumeration restrictions", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), + expected=True, + ) + + restriction = Restriction(options={"minInclusive": 42, "maxInclusive": 42}, type="bounds", base="decimal") + facet = Attribute(name="RefractionIndex", value=restriction) + ifc = ifcopenshell.file() + run( + "Strict numeric checking may be done with a bounds restriction", + facet=facet, + inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), + expected=True, + ) + + +class TestClassification: + def test_creating_a_classification_facet(self): + facet = Classification() + assert facet.asdict() == {} + facet = Classification(value="value", system="system") + assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}} + facet = Classification( + value="value", + system="system", + uri="https://test.com", + minOccurs="0", + maxOccurs="unbounded", + instructions="instructions", + ) + assert facet.asdict() == { + "value": {"simpleValue": "value"}, + "system": {"simpleValue": "system"}, + "@uri": "https://test.com", + "@minOccurs": "0", + "@maxOccurs": "unbounded", + "@instructions": "instructions", + } + + def test_filtering_using_a_classification_facet(self): + set_facet("classification") + + library = ifcopenshell.file() + system_a = library.createIfcClassification(Name="Foobar") + ref1 = library.createIfcClassificationReference(Identification="1", ReferencedSource=system_a) + ref11 = library.createIfcClassificationReference(Identification="11", ReferencedSource=ref1) + ref2 = library.createIfcClassificationReference(Identification="2", ReferencedSource=system_a) + ref22 = library.createIfcClassificationReference(Identification="22", ReferencedSource=ref2) + system_b = library.createIfcClassification(Name="Foobaz") + refx = library.createIfcClassificationReference(Identification="X", ReferencedSource=system_b) + + ifc = ifcopenshell.file() + project = ifc.createIfcProject() + system_a = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_a) + 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_a + ) + element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", ifc, product=element11, reference=ref11, classification=system_a + ) + element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", + ifc, + product=element22, + reference=ref22, + classification=system_a, + is_lightweight=False, + ) + + facet = Classification() + run( + "A classification facet with no data matches any classification 1/2", + facet=facet, + inst=element0, + expected=False, + ) + run( + "A classification facet with no data matches any classification 2/2", + facet=facet, + inst=element1, + expected=True, + ) + + facet = Classification(value="1") + run( + "Values should match exactly if lightweight classifications are used", + facet=facet, + inst=element1, + expected=True, + ) + + facet = Classification(value="2") + run( + "Values match subreferences if full classifications are used (e.g. EF_25_10 should match EF_25_10_25, EF_25_10_30, etc)", + facet=facet, + inst=element22, + expected=True, + ) + + facet = Classification(system="Foobar") + run("Systems should match exactly 1/5", facet=facet, inst=project, expected=True) + run("Systems should match exactly 2/5", facet=facet, inst=element0, expected=False) + run("Systems should match exactly 3/5", facet=facet, inst=element1, expected=True) + run("Systems should match exactly 4/5", facet=facet, inst=element11, expected=True) + run("Systems should match exactly 5/5", facet=facet, inst=element22, expected=True) + + restriction = Restriction(options="1.*", type="pattern") + facet = Classification(value=restriction) + run("Restrictions can be used for values 1/3", facet=facet, inst=element1, expected=True) + run("Restrictions can be used for values 2/3", facet=facet, inst=element11, expected=True) + run("Restrictions can be used for values 3/3", facet=facet, inst=element22, expected=False) + + restriction = Restriction(options="Foo.*", type="pattern") + facet = Classification(system=restriction) + run("Restrictions can be used for systems 1/2", facet=facet, inst=element0, expected=False) + run("Restrictions can be used for systems 2/2", facet=facet, inst=element1, expected=True) + + facet = Classification(system="Foobar", value="1") + run( + "Both system and value must match (all, not any) if specified 1/2", + facet=facet, + inst=element1, + expected=True, + ) + run( + "Both system and value must match (all, not any) if specified 2/2", + facet=facet, + inst=element11, + expected=False, + ) + + # IFC doesn't yet formally specify how inheritance and overrides work here. We follow these rules: + # https://github.com/buildingSMART/IFC4.3.x-development/issues/475 + 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_a + ) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system_a + ) + + system_b = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_b) + ifcopenshell.api.run( + "classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b + ) + + facet = Classification(value="11") + run("Occurrences override the type classification per system 1/3", facet=facet, inst=wall, expected=True) + facet = Classification(value="22") + run("Occurrences override the type classification per system 2/3", facet=facet, inst=wall, expected=False) + facet = Classification(value="X") + run("Occurrences override the type classification per system 3/3", facet=facet, inst=wall, expected=True) + + +class TestProperty: + def test_creating_a_property_facet(self): + facet = Property() + assert facet.asdict() == { + "propertySet": {"simpleValue": "Property_Set"}, + "name": {"simpleValue": "PropertyName"}, + } + facet = Property( + propertySet="propertySet", + name="name", + value="value", + measure="String", + uri="https://test.com", + minOccurs="0", + maxOccurs="unbounded", + instructions="instructions", + ) + assert facet.asdict() == { + "propertySet": {"simpleValue": "propertySet"}, + "name": {"simpleValue": "name"}, + "value": {"simpleValue": "value"}, + "@measure": "String", + "@uri": "https://test.com", + "@minOccurs": "0", + "@maxOccurs": "unbounded", + "@instructions": "instructions", + } + + def test_filtering_using_a_property_facet(self): + set_facet("property") + + ifc = ifcopenshell.file() + ifc.createIfcProject() + # Milli prefix used to check measurement conversions + lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") + areaunit = ifcopenshell.api.run( + "unit.add_si_unit", ifc, unit_type="AREAUNIT", name="SQUARE_METRE", prefix="MILLI" + ) + volumeunit = ifcopenshell.api.run( + "unit.add_si_unit", ifc, unit_type="VOLUMEUNIT", name="CUBIC_METRE", prefix="MILLI" + ) + timeunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="TIMEUNIT", name="SECOND") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[lengthunit, areaunit, volumeunit, timeunit]) + + # A name check by itself only checks that a property is non-null and non empty string + # The logic is that unfortunately most BIM users cannot differentiate between the two. + facet = Property(propertySet="Foo_Bar", name="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + run("", facet=facet, inst=element, expected=False) + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": None}) + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) + run("", facet=facet, inst=element, expected=True) + + # A simple value checks an exact case-sensitive match + facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar") + 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": "Bar"}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) + run("", facet=facet, inst=element, expected=False) + + # Simple values only check string matches + facet = Property(propertySet="Foo_Bar", name="Foo", value="1") + 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": "1"}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(1)}) + run("", facet=facet, inst=element, expected=False) + + # Restrictions are supported for property sets. If multiple are matched, all must satisfy requirements. + restriction = Restriction(options="Foo_.*", type="pattern") + facet = Property(propertySet=restriction, name="Foo") + 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": "Bar"}) + run("", facet=facet, inst=element, expected=True) + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Baz") + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) + run("", facet=facet, inst=element, expected=True) + + # Restrictions are supported for names. If multiple are matched, all must satisfy requirements. + restriction = Restriction(options="Foo.*", type="pattern") + facet = Property(propertySet="Foo_Bar", name=restriction, value="x") + 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={"Foobar": "x"}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "x"}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"}) + run("", facet=facet, inst=element, expected=False) + + # Restrictions are supported for values. If multiple are matched, all must satisfy requirements. + restriction1 = Restriction(options="Foo.*", type="pattern") + restriction2 = Restriction(options=["x", "y"], type="enumeration") + facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2) + 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={"Foobar": "x", "Foobaz": "y"}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "z"}) + run("", facet=facet, inst=element, expected=False) + + # Restrictions may be used to check basic data primitives + restriction = Restriction(options=[42.12], type="enumeration", base="decimal") + facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) + 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={"Foobar": 42.12}) + run("", facet=facet, inst=element, expected=True) + restriction = Restriction(options=[42], type="enumeration", base="integer") + facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42}) + run("", facet=facet, inst=element, expected=True) + restriction = Restriction(options=[True], type="enumeration", base="boolean") + facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": True}) + run("", facet=facet, inst=element, expected=True) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": False}) + run("", facet=facet, inst=element, expected=False) + + # When measure is not specified, no unit conversion is done and only primitives are checked + restriction = Restriction(options=[42.12], type="enumeration", base="decimal") + facet = Property(propertySet="Foo_Bar", name="Foobar", value=restriction) + 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={"Foobar": 42.12}) + run("", facet=facet, inst=element, expected=True) + + # Measure may be used to specify an IFC data type + restriction = Restriction(options=[2], type="enumeration", base="decimal") + facet = Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Time") + 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.createIfcMassMeasure(2)}) + run("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcTimeMeasure(2)}) + run("", facet=facet, inst=element, expected=True) + + # Measure also implies that a unit matters, and so a conversion shall take place to SI units + restriction = Restriction(options=[2], type="enumeration", base="decimal") + facet = Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Length") + 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("", facet=facet, inst=element, expected=False) + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)}) + run("", facet=facet, inst=element, expected=True) + + # The facet checks inherited properties from 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) + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) + facet = Property(propertySet="Foo_Bar", name="Foo") + run("", facet=facet, inst=wall, expected=True) + run("", facet=facet, inst=wall_type, expected=True) + + # The facet checks overriden properties from the occurrence + 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) + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) + pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar") + ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) + facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar") + run("", facet=facet, inst=wall, expected=True) + run("", facet=facet, inst=wall_type, expected=False) + + +class TestMaterial: + def test_creating_a_material_facet(self): + facet = Material() + assert facet.asdict() == {} + facet = Material( + value="value", uri="https://test.com", minOccurs="0", maxOccurs="unbounded", instructions="instructions" + ) + assert facet.asdict() == { + "value": {"simpleValue": "value"}, + "@uri": "https://test.com", + "@minOccurs": "0", + "@maxOccurs": "unbounded", + "@instructions": "instructions", + } + + def test_filtering_using_a_material_facet(self): + set_facet("material") + + facet = Material() + ifc = ifcopenshell.file() + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + run("Elements without a material always fail", facet=facet, inst=element, expected=False) + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + run("Elements with any material will pass an empty material facet", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Material(value="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + run("Material with no data will fail a value check", facet=facet, inst=element, expected=False) + material.Name = "Foo" + run("A material name may pass the value check", facet=facet, inst=element, expected=True) + material.Name = "Bar" + material.Category = "Foo" + run("A material category may pass the value check", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Material(value="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialList") + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + run("A material list with no data will fail a value check", facet=facet, inst=element, expected=False) + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.add_list_item", ifc, material_list=material_set, material=material) + material.Name = "Foo" + run("Any material Name in a list will pass a value check", facet=facet, inst=element, expected=True) + material.Name = "Bar" + material.Category = "Foo" + run("Any material Category in a list will pass a value check", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Material(value="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialLayerSet") + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + material = ifcopenshell.api.run("material.add_material", ifc) + layer = ifcopenshell.api.run("material.add_layer", ifc, layer_set=material_set, material=material) + layer.Name = "Foo" + run("Any layer Name in a layer set will pass a value check", facet=facet, inst=element, expected=True) + layer.Name = "Bar" + layer.Category = "Foo" + run("Any layer Category in a layer set will pass a value check", facet=facet, inst=element, expected=True) + layer.Category = "Bar" + material.Name = "Foo" + run("Any material Name in a layer set will pass a value check", facet=facet, inst=element, expected=True) + material.Name = "Bar" + material.Category = "Foo" + run("Any material Category in a layer set will pass a value check", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Material(value="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialProfileSet") + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + material = ifcopenshell.api.run("material.add_material", ifc) + profile = ifcopenshell.api.run("material.add_profile", ifc, profile_set=material_set, material=material) + profile.Name = "Foo" + profile.Profile = ifc.createIfcCircleProfileDef("AREA", None, None, 1) + run("Any profile Name in a profile set will pass a value check", facet=facet, inst=element, expected=True) + profile.Name = "Bar" + profile.Category = "Foo" + run("Any profile Category in a profile set will pass a value check", facet=facet, inst=element, expected=True) + profile.Category = "Bar" + material.Name = "Foo" + run("Any material Name in a profile set will pass a value check", facet=facet, inst=element, expected=True) + material.Name = "Bar" + material.Category = "Foo" + run("Any material category in a profile set will pass a value check", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + facet = Material(value="Foo") + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialConstituentSet") + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + run("A constituent set with no data will fail a value check", facet=facet, inst=element, expected=False) + material = ifcopenshell.api.run("material.add_material", ifc) + constituent = ifcopenshell.api.run( + "material.add_constituent", ifc, constituent_set=material_set, material=material + ) + constituent.Name = "Foo" + run( + "Any constituent Name in a constituent set will pass a value check", + facet=facet, + inst=element, + expected=True, + ) + constituent.Name = "Bar" + constituent.Category = "Foo" + run( + "Any constituent Category in a constituent set will pass a value check", + facet=facet, + inst=element, + expected=True, + ) + constituent.Category = "Bar" + material.Name = "Foo" + run("Any material Name in a constituent set will pass a value check", facet=facet, inst=element, expected=True) + material.Name = "Bar" + material.Category = "Foo" + run( + "Any material Category in a constituent set will pass a value check", + facet=facet, + inst=element, + expected=True, + ) + + ifc = ifcopenshell.file() + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) + material.Name = "Foo" + facet = Material(value="Foo") + run("Occurrences can inherit materials from their types", facet=facet, inst=element, expected=True) + + ifc = ifcopenshell.file() + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) + material.Name = "Bar" + material = ifcopenshell.api.run("material.add_material", ifc) + ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + material.Name = "Foo" + facet = Material(value="Foo") + run("Occurrences can override materials from their types", facet=facet, inst=element, expected=True) + + +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"} + + 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") + 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) + + # 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) + + # 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) + + # 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) + ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group) + run("", 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") + ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group) + run("", 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) + + # 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) + + +class TestRestriction: + def test_enumeration(self): + restriction = Restriction(options=["foo", "bar"], type="enumeration") + assert restriction == "foo" + assert restriction == "bar" + assert restriction != "baz" + + def test_bounds(self): + restriction = Restriction(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") + assert restriction == 0 + assert restriction != 10 + assert restriction == 5 + assert restriction != -1 + + def test_pattern(self): + restriction = Restriction(options="[A-Z]{2}[0-9]{2}", type="pattern") + assert restriction == "AB01" + assert restriction != "AB" + assert restriction != "01" diff --git a/src/ifctester/test/test_ids.py b/src/ifctester/test/test_ids.py index 31f521542e..04c8f3ccdc 100644 --- a/src/ifctester/test/test_ids.py +++ b/src/ifctester/test/test_ids.py @@ -1,167 +1,33 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Thomas Krijnen +# IfcTester - IDS based model auditing +# Copyright (C) 2021-2022 Thomas Krijnen , Dion Moult # -# This file is part of IfcOpenShell. +# This file is part of IfcTester. # -# IfcOpenShell is free software: you can redistribute it and/or modify +# IfcTester is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# IfcOpenShell is distributed in the hope that it will be useful, +# IfcTester is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along with IfcOpenShell. If not, see . +# along with IfcTester. If not, see . import os import pytest -import logging -import unittest -import tempfile import xmlschema import ifcopenshell -import ifcopenshell.api -from bcf import bcfxml -from ifcopenshell import ids - -IFC_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IFC/", "IFC4_Wall_3_with_properties.ifc") -IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_all_fields.xml") - -logger = logging.getLogger("IDS_Logger") -# logging.basicConfig(level=logging.INFO, format="%(message)s") -# logging.basicConfig(filename=os.path.join(os.path.dirname(__file__), "log.txt"), level=logging.INFO, format="%(message)s") - -file = open(os.path.join(tempfile.gettempdir(), "test.ifc"), "w") -file.write(IFC_URL) -file.close() -ifc_file = ifcopenshell.open(IFC_URL) -os.remove(os.path.join(tempfile.gettempdir(), "test.ifc")) +from ifctester import ids -def set_facet(facet): - pass - -def run(name, *, facet, inst, expected): - assert bool(facet(inst)) is expected - - -class TestIdsParsing(unittest.TestCase): - def test_parse_basic_ids(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_all_fields.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual(type(ids_file).__name__, "ids") - - def test_parse_entity_facet(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_entity.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "IfcWall") - - def test_parse_predefinedType_facet(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_predefinedtype.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements[0].node["predefinedType"]["simpleValue"], "CLADDING" - ) - - def test_parse_property_facet(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_property.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements[0].node["propertySet"]["simpleValue"], "Test_PropertySet" - ) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual(ids_file.specifications[0].requirements[0].node["value"]["simpleValue"], "Test_Value") - - def test_parse_material_facet(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_material.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["value"]["simpleValue"], "Test_Material") - - def test_parse_classification_facet(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_classification.xml") - ids_file = ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements[0].node["value"]["simpleValue"], "Test_Classification" - ) - self.assertEqual(ids_file.specifications[0].requirements[0].node["system"]["simpleValue"], "Test_System") - +class TestIds: def test_failing_on_opening_invalid_ids_data(self): with pytest.raises(xmlschema.validators.exceptions.XMLSchemaValidationError): ids.open("""""") - """ Saving parsed IDS to IDS.xml """ - - def test_parsed_ids_to_xml(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files", "IDS", "IDS_Wall_needs_all_fields.xml") - ids_file = ids.open(IDS_URL) - fn = "output.xml" - result = ids_file.to_xml(fn) - assert os.path.isfile(fn) - os.remove(fn) - self.assertTrue(result) - - def test_parsed_ids_to_string(self): - return # TODO - IDS_URL = os.path.join(os.path.dirname(__file__), "Sample-BIM-Files", "IDS", "IDS_Wall_needs_all_fields.xml") - ids_file = ids.open(IDS_URL) - output = ids_file.to_string() - assert output and "http://standards.buildingsmart.org/IDS" in output - - """ Parsing IDS files with restrictions """ - - def test_parse_restrictions_enumeration(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), - "Sample-BIM-Files/IDS/", - "IDS_Wall_needs_property_with_restriction_enumeration.xml", - ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - [ - x["@value"] - for x in ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["enumeration"] - ], - ["testA", "testB"], - ) - - def test_parse_restrictions_bounds(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_property_with_restriction_bounds.xml" - ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["minInclusive"]["@value"], - "0", - ) - - def test_parse_restrictions_pattern_simple(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_property_with_restriction_pattern.xml" - ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["pattern"]["@value"], - "[A-Z]{2,4}", - ) - - -class TestIdsAuthoring(unittest.TestCase): def test_create_an_ids_with_minimal_information(self): specs = ids.Ids() assert specs.asdict() == { @@ -218,1017 +84,16 @@ class TestIdsAuthoring(unittest.TestCase): with pytest.raises(xmlschema.validators.exceptions.XMLSchemaChildrenValidationError): specs.to_string() - def test_create_specification_with_minimal_information(self): - spec = ids.Specification() - assert spec.asdict() == { - "@name": "Unnamed", - "@ifcVersion": ["IFC2X3", "IFC4"], - "applicability": {}, - "requirements": {}, - } - - def test_create_specification_with_all_possible_information(self): - spec = ids.Specification( - name="name", - minOccurs="0", - maxOccurs="unbounded", - ifcVersion="IFC4", - identifier="identifier", - description="description", - instructions="instructions", - ) - assert spec.asdict() == { - "@name": "name", - "@minOccurs": "0", - "@maxOccurs": "unbounded", - "@ifcVersion": "IFC4", - "@identifier": "identifier", - "@description": "description", - "@instructions": "instructions", - "applicability": {}, - "requirements": {}, - } - - def test_creating_an_entity_facet(self): - facet = ids.Entity(name="IfcName") - assert facet.asdict() == {"name": {"simpleValue": "IfcName"}} - facet = ids.Entity(name="IfcName", predefinedType="predefinedType", instructions="instructions") - assert facet.asdict() == { - "name": {"simpleValue": "IfcName"}, - "predefinedType": {"simpleValue": "predefinedType"}, - "@instructions": "instructions", - } - - def test_filtering_using_an_entity_facet(self): - set_facet("entity") - - ifc = ifcopenshell.file() - facet = ids.Entity(name="IFCRABBIT") - run("Invalid entities always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) - - ifc = ifcopenshell.file() - facet = ids.Entity(name="IFCWALL") - run("A matching entity should pass", facet=facet, inst=ifc.createIfcWall(), expected=True) - ifc = ifcopenshell.file() - run( - "An matching entity should pass regardless of predefined type", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), - expected=True, - ) - ifc = ifcopenshell.file() - run( - "An entity not matching the specified class should fail", - facet=facet, - inst=ifc.createIfcSlab(), - expected=False, - ) - # TODO: Some votes to prefer inheritance (For: Moult, Evandro, Artur) - # Possible argument: CAD tools don't understand IFC - ifc = ifcopenshell.file() - run( - "Subclasses are not considered as matching", - facet=facet, - inst=ifc.createIfcWallStandardCase(), - expected=False, - ) - - # TODO But in that case why are the enumerations for things like partOf using the IFC capitalisation? - facet = ids.Entity(name="IfcWall") - ifc = ifcopenshell.file() - run( - "Entities must be specified as uppercase strings", - facet=facet, - inst=ifc.createIfcWall(), - expected=False, - ) - - facet = ids.Entity(name="IFCWALL", predefinedType="SOLIDWALL") - ifc = ifcopenshell.file() - run( - "A matching predefined type should pass", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), - expected=True, - ) - ifc = ifcopenshell.file() - run( - "A null predefined type should always fail a specified predefined types", - facet=facet, - inst=ifc.createIfcWall(), - expected=False, - ) - ifc = ifcopenshell.file() - run( - "An entity not matching a specified predefined type will fail", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="PARTITIONING"), - expected=False, - ) - - facet = ids.Entity(name="IFCWALL", predefinedType="solidwall") - ifc = ifcopenshell.file() - run( - "A predefined type from an enumeration must be uppercase", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="SOLIDWALL"), - expected=False, - ) - - facet = ids.Entity(name="IFCWALL", predefinedType="WALDO") - ifc = ifcopenshell.file() - run( - "A predefined type may specify a user-defined object type", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"), - expected=True, - ) - - facet = ids.Entity(name="IFCWALL", predefinedType="WALDO") - ifc = ifcopenshell.file() - run( - "User-defined types are checked case sensitively", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="waldo"), - expected=False, - ) - - facet = ids.Entity(name="IFCWALLTYPE", predefinedType="WALDO") - ifc = ifcopenshell.file() - run( - "A predefined type may specify a user-defined element type", - facet=facet, - inst=ifc.createIfcWallType(PredefinedType="USERDEFINED", ElementType="WALDO"), - expected=True, - ) - - facet = ids.Entity(name="IFCTASKTYPE", predefinedType="TASKY") - ifc = ifcopenshell.file() - run( - "A predefined type may specify a user-defined process type", - facet=facet, - inst=ifc.createIfcTaskType(PredefinedType="USERDEFINED", ProcessType="TASKY"), - expected=True, - ) - - facet = ids.Entity(name="IFCWALL", predefinedType="USERDEFINED") - ifc = ifcopenshell.file() - run( - "A predefined type must always specify a meaningful type, not USERDEFINED itself", - facet=facet, - inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"), - expected=False, - ) - - ifc = ifcopenshell.file() - wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="X") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) - facet = ids.Entity(name="IFCWALL", predefinedType="X") - run("Inherited predefined types should pass", facet=facet, inst=wall, expected=True) - - ifc = ifcopenshell.file() - wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="X") - wall_type = ifcopenshell.api.run( - "root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="NOTDEFINED" - ) - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) - facet = ids.Entity(name="IFCWALL", predefinedType="X") - run("Overridden predefined types should pass", facet=facet, inst=wall, expected=True) - - restriction = ids.Restriction(options=["IFCWALL", "IFCSLAB"], type="enumeration") - facet = ids.Entity(name=restriction) - ifc = ifcopenshell.file() - run("Entities can be specified as an enumeration 1/3", facet=facet, inst=ifc.createIfcWall(), expected=True) - ifc = ifcopenshell.file() - run("Entities can be specified as an enumeration 2/3", facet=facet, inst=ifc.createIfcSlab(), expected=True) - ifc = ifcopenshell.file() - run("Entities can be specified as an enumeration 3/3", facet=facet, inst=ifc.createIfcBeam(), expected=False) - - restriction = ids.Restriction(options="IFC.*TYPE", type="pattern") - facet = ids.Entity(name=restriction) - ifc = ifcopenshell.file() - run("Entities can be specified as a XSD regex pattern 1/2", facet=facet, inst=ifc.createIfcWall(), expected=False) - ifc = ifcopenshell.file() - run("Entities can be specified as a XSD regex pattern 2/2", facet=facet, inst=ifc.createIfcWallType(), expected=True) - - restriction = ids.Restriction(options="FOO.*", type="pattern") - facet = ids.Entity(name="IFCWALL", predefinedType=restriction) - ifc = ifcopenshell.file() - wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAR") - run("Restrictions an be specified for the predefined type 1/3", facet=facet, inst=wall, expected=True) - ifc = ifcopenshell.file() - wall2 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAZ") - run("Restrictions an be specified for the predefined type 2/3", facet=facet, inst=wall2, expected=True) - ifc = ifcopenshell.file() - wall3 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="BAZFOO") - run("Restrictions an be specified for the predefined type 3/3", facet=facet, inst=wall3, expected=False) - - def test_creating_an_attribute_facet(self): - attribute = ids.Attribute(name="name") - assert attribute.asdict() == {"name": {"simpleValue": "name"}} - attribute = ids.Attribute(name="name", value="value") - assert attribute.asdict() == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}} - attribute = ids.Attribute( - name="name", value="value", minOccurs="0", maxOccurs="unbounded", instructions="instructions" - ) - assert attribute.asdict() == { - "name": {"simpleValue": "name"}, - "value": {"simpleValue": "value"}, - "@minOccurs": "0", - "@maxOccurs": "unbounded", - "@instructions": "instructions", - } - - def test_filtering_using_an_attribute_facet(self): - set_facet("attribute") - - ifc = ifcopenshell.file() - facet = ids.Attribute(name="Foobar") - run("Invalid attribute names always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) - - ifc = ifcopenshell.file() - facet = ids.Attribute(name="Name") - run("Attributes with a string value should pass", facet=facet, inst=ifc.createIfcWall(Name="Foobar"), expected=True) - ifc = ifcopenshell.file() - run("Attributes with null values always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) - # The logic is that unfortunately most BIM users cannot differentiate between the two. - ifc = ifcopenshell.file() - run("Attributes with empty strings always fail", facet=facet, inst=ifc.createIfcWall(Name=""), expected=False) - - facet = ids.Attribute(name="CountValue") - ifc = ifcopenshell.file() - run("Attributes with a zero number have meaning and should pass", facet=facet, inst=ifc.createIfcQuantityCount(Name="Foobar", CountValue=0), expected=True) - - facet = ids.Attribute(name="IsCritical") - ifc = ifcopenshell.file() - element = ifc.createIfcTaskTime(IsCritical=True) - run("Attributes with a boolean true should pass", facet=facet, inst=element, expected=True) - element.IsCritical = False - run("Attributes with a boolean false should pass", facet=facet, inst=element, expected=True) - - facet = ids.Attribute(name="RelatingPriorities") - ifc = ifcopenshell.file() - run("Attributes with an empty list always fail", facet=facet, inst=ifc.createIfcRelConnectsPathElements(RelatingElement=ifc.createIfcWall(), RelatedElement=ifc.createIfcWall(), RelatingPriorities=[], RelatedPriorities=[], RelatedConnectionType="ATSTART", RelatingConnectionType="ATEND"), expected=False) - - facet = ids.Attribute(name="LayerStyles") - ifc = ifcopenshell.file() - item = ifc.createIfcCartesianPoint([0., 0., 0.]) - layer = ifc.createIfcPresentationLayerWithStyle("Foo", None, [item], None, True, False, False, []) - run("Attributes with an empty set always fail", facet=facet, inst=layer, expected=False) - - layer.LayerOn = "UNKNOWN" - facet = ids.Attribute(name="LayerOn") - run("Attributes with a logical unknown always fail", facet=facet, inst=layer, expected=False) - - facet = ids.Attribute(name="ScheduleDuration") - ifc = ifcopenshell.file() - element = ifc.createIfcTaskTime(ScheduleDuration="P0D") - run("Attributes with a zero duration should pass", facet=facet, inst=element, expected=True) - - facet = ids.Attribute(name="TaskTime") - ifc = ifcopenshell.file() - element = ifc.createIfcTask(IsMilestone=True, TaskTime=ifc.createIfcTaskTime()) - run("Attributes referencing an object should pass", facet=facet, inst=element, expected=True) - - facet = ids.Attribute(name="DiffuseColour") - ifc = ifcopenshell.file() - rgb = ifc.createIfcColourRgb(None, 1, 1, 1) - run("Attributes with a select referencing an object should pass", facet=facet, inst=ifc.createIfcSurfaceStyleRendering(SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcColourRgb(None, 1, 1, 1)), expected=True) - - ifc = ifcopenshell.file() - rgb = ifc.createIfcColourRgb(None, 1, 1, 1) - run("Attributes with a select referencing a primitive should pass", facet=facet, inst=ifc.createIfcSurfaceStyleRendering(SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcNormalisedRatioMeasure(0.5)), expected=True) - - ifc = ifcopenshell.file() - facet = ids.Attribute(name="EngagedIn") - person = ifc.createIfcPerson() - organisation = ifc.createIfcOrganization(Name="Foo") - ifc.createIfcPersonAndOrganization(ThePerson=person, TheOrganization=organisation) - run("Inverse attributes cannot be checked and always fail", facet=facet, inst=person, expected=False) - - ifc = ifcopenshell.file() - facet = ids.Attribute(name="Dim") - run("Derived attributes cannot be checked and always fail", facet=facet, inst=ifc.createIfcCartesianPoint([0., 0., 0.]), expected=False) - - facet = ids.Attribute(name="Name", value="Foobar") - ifc = ifcopenshell.file() - run("Attributes should check strings case sensitively 1/2", facet=facet, inst=ifc.createIfcWall(Name="Foobar"), expected=True) - ifc = ifcopenshell.file() - run("Attributes should check strings case sensitively 2/2", facet=facet, inst=ifc.createIfcWall(Name="foobar"), expected=False) - - ifc = ifcopenshell.file() - facet = ids.Attribute(name="Name", value="♫") - run("Non-ascii characters are treated without encoding", facet=facet, inst=ifc.createIfcWall(Name="♫"), expected=True) - - facet = ids.Attribute(name="TaskTime", value="Foobar") - ifc = ifcopenshell.file() - run("Value checks always fail for objects", facet=facet, inst=ifc.createIfcTask(IsMilestone=False, TaskTime=ifc.createIfcTaskTime()), expected=False) - - facet = ids.Attribute(name="DiffuseColour", value="Foobar") - ifc = ifcopenshell.file() - rgb = ifc.createIfcColourRgb(None, 1, 1, 1) - run("Value checks always fail for selects", facet=facet, inst=ifc.createIfcSurfaceStyleRendering(SurfaceColour=rgb, ReflectanceMethod="FLAT", DiffuseColour=ifc.createIfcNormalisedRatioMeasure(0.5)), expected=False) - - facet = ids.Attribute(name="Coordinates", value="Foobar") - ifc = ifcopenshell.file() - run("Value checks always fail for lists", facet=facet, inst=ifc.createIfcCartesianPoint([0., 0., 0.]), expected=False) - - # TODO continue from here - - global_id = ifcopenshell.guid.new() - facet = ids.Attribute(name="GlobalId", value=global_id) - ifc = ifcopenshell.file() - run("GlobalIds are treated as strings and not expanded", facet=facet, inst=ifc.createIfcWall(GlobalId=global_id), expected=True) - - # 255 characters - identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345" - facet = ids.Attribute(name="Identification", value=identifier + "_extra_characters") - ifc = ifcopenshell.file() - run("IDS does not handle string truncation such as for identifiers", facet=facet, inst=ifc.createIfcPerson(Identification=identifier), expected=False) - - facet = ids.Attribute(name="RefractionIndex", value="42") - ifc = ifcopenshell.file() - run("Numeric values are checked using type casting 1/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), expected=True) - facet = ids.Attribute(name="RefractionIndex", value="42.") - ifc = ifcopenshell.file() - run("Numeric values are checked using type casting 2/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0), expected=True) - facet = ids.Attribute(name="RefractionIndex", value="42.0") - ifc = ifcopenshell.file() - run("Numeric values are checked using type casting 3/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.0), expected=True) - facet = ids.Attribute(name="RefractionIndex", value="42") - ifc = ifcopenshell.file() - run("Numeric values are checked using type casting 4/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.3), expected=False) - facet = ids.Attribute(name="RefractionIndex", value="42,3") - ifc = ifcopenshell.file() - run("Only specifically formatted numbers are allowed 1/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42.3), expected=False) - facet = ids.Attribute(name="RefractionIndex", value="123,4.5") - ifc = ifcopenshell.file() - run("Only specifically formatted numbers are allowed 2/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), expected=False) - facet = ids.Attribute(name="RefractionIndex", value="1.2345e3") - ifc = ifcopenshell.file() - run("Only specifically formatted numbers are allowed 3/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), expected=True) - facet = ids.Attribute(name="RefractionIndex", value="1.2345E3") - ifc = ifcopenshell.file() - run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=1234.5), expected=True) - - facet = ids.Attribute(name="NumberOfRisers", value="42") - ifc = ifcopenshell.file() - run("Integers follow the same rules as numbers", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), expected=True) - facet = ids.Attribute(name="NumberOfRisers", value="42.0") - ifc = ifcopenshell.file() - run("Integers follow the same rules as numbers 2/2", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), expected=True) - - facet = ids.Attribute(name="NumberOfRisers", value="42.3") - ifc = ifcopenshell.file() - run("Integers are always floored when cast 1/2", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), expected=True) - facet = ids.Attribute(name="NumberOfRisers", value="42.7") - ifc = ifcopenshell.file() - run("Integers are always floored when cast 2/2", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), expected=True) - - facet = ids.Attribute(name="NumberOfRisers", value="42.7") - ifc = ifcopenshell.file() - run("Integers are always floored when cast 2/2", facet=facet, inst=ifc.createIfcStairFlight(NumberOfRisers=42), expected=True) - - facet = ids.Attribute(name="IsMilestone", value="TRUE") - ifc = ifcopenshell.file() - element = ifc.createIfcTask(IsMilestone=False) - run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False) - facet = ids.Attribute(name="IsMilestone", value="FALSE") - run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True) - facet = ids.Attribute(name="IsMilestone", value="False") - run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=False) - - facet = ids.Attribute(name="EditionDate", value="2022-01-01") - ifc = ifcopenshell.file() - run("Dates are treated as strings 1/2", facet=facet, inst=ifc.createIfcClassification(Name="Name", EditionDate="2022-01-01"), expected=True) - ifc = ifcopenshell.file() - run("Dates are treated as strings 1/2", facet=facet, inst=ifc.createIfcClassification(Name="Name", EditionDate="2022-01-01+00:00"), expected=False) - - facet = ids.Attribute(name="ScheduleDuration", value="PT16H") - ifc = ifcopenshell.file() - run("Durations are treated as strings 1/2", facet=facet, inst=ifc.createIfcClassification(Name="Name", EditionDate="PT16H"), expected=False) - ifc = ifcopenshell.file() - run("Durations are treated as strings 2/2", facet=facet, inst=ifc.createIfcClassification(Name="Name", EditionDate="P2D"), expected=False) - - restriction = ids.Restriction(options=".*Name.*", type="pattern") - facet = ids.Attribute(name=restriction) - ifc = ifcopenshell.file() - run("Name restrictions may be used 1/4", facet=facet, inst=ifc.createIfcMaterialLayerSet(MaterialLayers=[ifc.createIfcMaterialLayer(LayerThickness=1)], LayerSetName="Foo"), expected=True) - ifc = ifcopenshell.file() - run("Name restrictions may be used 2/4", facet=facet, inst=ifc.createIfcMaterialConstituentSet(Name="Foo"), expected=True) - - restriction = ids.Restriction(options=["Name", "Description"], type="enumeration") - facet = ids.Attribute(name=restriction) - ifc = ifcopenshell.file() - run("Name restrictions may be used 3/4", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=False) - ifc = ifcopenshell.file() - run("Name restrictions may be used 4/4", facet=facet, inst=ifc.createIfcWall(Name="Foo", Description="Bar"), expected=True) - - restriction = ids.Restriction(options=["Foo", "Bar"], type="enumeration") - facet = ids.Attribute(name="Name", value=restriction) - ifc = ifcopenshell.file() - run("Value restrictions may be used 1/3", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=True) - ifc = ifcopenshell.file() - run("Value restrictions may be used 2/3", facet=facet, inst=ifc.createIfcWall(Name="Bar"), expected=True) - ifc = ifcopenshell.file() - run("Value restrictions may be used 3/3", facet=facet, inst=ifc.createIfcWall(Name="Foobar"), expected=False) - - ifc = ifcopenshell.file() - 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) - wall_type.Description = "Foobar" - run("Attributes are not inherited by the occurrence", facet=facet, inst=wall, expected=False) - - restriction = ids.Restriction(options=["42"], type="enumeration", base="string") - facet = ids.Attribute(name="RefractionIndex", value=restriction) - ifc = ifcopenshell.file() - run("Typecast checking may also occur within enumeration restrictions", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), expected=True) - - restriction = ids.Restriction( - options={"minInclusive": 42, "maxInclusive": 42}, type="bounds", base="decimal" - ) - facet = ids.Attribute(name="RefractionIndex", value=restriction) - ifc = ifcopenshell.file() - run("Strict numeric checking may be done with a bounds restriction", facet=facet, inst=ifc.createIfcSurfaceStyleRefraction(RefractionIndex=42), expected=True) - - def test_creating_a_classification_facet(self): - facet = ids.Classification() - assert facet.asdict() == {} - facet = ids.Classification(value="value", system="system") - assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}} - facet = ids.Classification( - value="value", - system="system", - uri="https://test.com", - minOccurs="0", - maxOccurs="unbounded", - instructions="instructions", - ) - assert facet.asdict() == { - "value": {"simpleValue": "value"}, - "system": {"simpleValue": "system"}, - "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", - "@instructions": "instructions", - } - - def test_filtering_using_a_classification_facet(self): - set_facet("classification") - - library = ifcopenshell.file() - system_a = library.createIfcClassification(Name="Foobar") - ref1 = library.createIfcClassificationReference(Identification="1", ReferencedSource=system_a) - ref11 = library.createIfcClassificationReference(Identification="11", ReferencedSource=ref1) - ref2 = library.createIfcClassificationReference(Identification="2", ReferencedSource=system_a) - ref22 = library.createIfcClassificationReference(Identification="22", ReferencedSource=ref2) - system_b = library.createIfcClassification(Name="Foobaz") - refx = library.createIfcClassificationReference(Identification="X", ReferencedSource=system_b) - - ifc = ifcopenshell.file() - project = ifc.createIfcProject() - system_a = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_a) - 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_a - ) - element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - ifcopenshell.api.run( - "classification.add_reference", ifc, product=element11, reference=ref11, classification=system_a - ) - element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - ifcopenshell.api.run( - "classification.add_reference", - ifc, - product=element22, - reference=ref22, - classification=system_a, - is_lightweight=False, - ) - - facet = ids.Classification() - run("A classification facet with no data matches any classification 1/2", facet=facet, inst=element0, expected=False) - run("A classification facet with no data matches any classification 2/2", facet=facet, inst=element1, expected=True) - - facet = ids.Classification(value="1") - run("Values should match exactly if lightweight classifications are used", facet=facet, inst=element1, expected=True) - - facet = ids.Classification(value="2") - run("Values match subreferences if full classifications are used (e.g. EF_25_10 should match EF_25_10_25, EF_25_10_30, etc)", facet=facet, inst=element22, expected=True) - - facet = ids.Classification(system="Foobar") - run("Systems should match exactly 1/5", facet=facet, inst=project, expected=True) - run("Systems should match exactly 2/5", facet=facet, inst=element0, expected=False) - run("Systems should match exactly 3/5", facet=facet, inst=element1, expected=True) - run("Systems should match exactly 4/5", facet=facet, inst=element11, expected=True) - run("Systems should match exactly 5/5", facet=facet, inst=element22, expected=True) - - restriction = ids.Restriction(options="1.*", type="pattern") - facet = ids.Classification(value=restriction) - run("Restrictions can be used for values 1/3", facet=facet, inst=element1, expected=True) - run("Restrictions can be used for values 2/3", facet=facet, inst=element11, expected=True) - run("Restrictions can be used for values 3/3", facet=facet, inst=element22, expected=False) - - restriction = ids.Restriction(options="Foo.*", type="pattern") - facet = ids.Classification(system=restriction) - run("Restrictions can be used for systems 1/2", facet=facet, inst=element0, expected=False) - run("Restrictions can be used for systems 2/2", facet=facet, inst=element1, expected=True) - - facet = ids.Classification(system="Foobar", value="1") - run("Both system and value must match (all, not any) if specified 1/2", facet=facet, inst=element1, expected=True) - run("Both system and value must match (all, not any) if specified 2/2", facet=facet, inst=element11, expected=False) - - # IFC doesn't yet formally specify how inheritance and overrides work here. We follow these rules: - # https://github.com/buildingSMART/IFC4.3.x-development/issues/475 - 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_a) - ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system_a - ) - - system_b = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_b) - ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b - ) - - facet = ids.Classification(value="11") - run("Occurrences override the type classification per system 1/3", facet=facet, inst=wall, expected=True) - facet = ids.Classification(value="22") - run("Occurrences override the type classification per system 2/3", facet=facet, inst=wall, expected=False) - facet = ids.Classification(value="X") - run("Occurrences override the type classification per system 3/3", facet=facet, inst=wall, expected=True) - - def test_creating_a_property_facet(self): - facet = ids.Property() - assert facet.asdict() == { - "propertySet": {"simpleValue": "Property_Set"}, - "name": {"simpleValue": "PropertyName"}, - } - facet = ids.Property( - propertySet="propertySet", - name="name", - value="value", - measure="String", - uri="https://test.com", - minOccurs="0", - maxOccurs="unbounded", - instructions="instructions", - ) - assert facet.asdict() == { - "propertySet": {"simpleValue": "propertySet"}, - "name": {"simpleValue": "name"}, - "value": {"simpleValue": "value"}, - "@measure": "String", - "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", - "@instructions": "instructions", - } - - def test_filtering_using_a_property_facet(self): - set_facet("property") - - ifc = ifcopenshell.file() - ifc.createIfcProject() - # Milli prefix used to check measurement conversions - lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") - areaunit = ifcopenshell.api.run( - "unit.add_si_unit", ifc, unit_type="AREAUNIT", name="SQUARE_METRE", prefix="MILLI" - ) - volumeunit = ifcopenshell.api.run( - "unit.add_si_unit", ifc, unit_type="VOLUMEUNIT", name="CUBIC_METRE", prefix="MILLI" - ) - timeunit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="TIMEUNIT", name="SECOND") - ifcopenshell.api.run("unit.assign_unit", ifc, units=[lengthunit, areaunit, volumeunit, timeunit]) - - # A name check by itself only checks that a property is non-null and non empty string - # The logic is that unfortunately most BIM users cannot differentiate between the two. - facet = ids.Property(propertySet="Foo_Bar", name="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - run("", facet=facet, inst=element, expected=False) - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": None}) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - run("", facet=facet, inst=element, expected=True) - - # A simple value checks an exact case-sensitive match - facet = ids.Property(propertySet="Foo_Bar", name="Foo", value="Bar") - 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": "Bar"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) - run("", facet=facet, inst=element, expected=False) - - # Simple values only check string matches - facet = ids.Property(propertySet="Foo_Bar", name="Foo", value="1") - 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": "1"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(1)}) - run("", facet=facet, inst=element, expected=False) - - # Restrictions are supported for property sets. If multiple are matched, all must satisfy requirements. - restriction = ids.Restriction(options="Foo_.*", type="pattern") - facet = ids.Property(propertySet=restriction, name="Foo") - 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": "Bar"}) - run("", facet=facet, inst=element, expected=True) - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Baz") - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - run("", facet=facet, inst=element, expected=True) - - # Restrictions are supported for names. If multiple are matched, all must satisfy requirements. - restriction = ids.Restriction(options="Foo.*", type="pattern") - facet = ids.Property(propertySet="Foo_Bar", name=restriction, value="x") - 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={"Foobar": "x"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "x"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"}) - run("", facet=facet, inst=element, expected=False) - - # Restrictions are supported for values. If multiple are matched, all must satisfy requirements. - restriction1 = ids.Restriction(options="Foo.*", type="pattern") - restriction2 = ids.Restriction(options=["x", "y"], type="enumeration") - facet = ids.Property(propertySet="Foo_Bar", name=restriction1, value=restriction2) - 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={"Foobar": "x", "Foobaz": "y"}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "z"}) - run("", facet=facet, inst=element, expected=False) - - # Restrictions may be used to check basic data primitives - restriction = ids.Restriction(options=[42.12], type="enumeration", base="decimal") - facet = ids.Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - 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={"Foobar": 42.12}) - run("", facet=facet, inst=element, expected=True) - restriction = ids.Restriction(options=[42], type="enumeration", base="integer") - facet = ids.Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": 42}) - run("", facet=facet, inst=element, expected=True) - restriction = ids.Restriction(options=[True], type="enumeration", base="boolean") - facet = ids.Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": True}) - run("", facet=facet, inst=element, expected=True) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": False}) - run("", facet=facet, inst=element, expected=False) - - # When measure is not specified, no unit conversion is done and only primitives are checked - restriction = ids.Restriction(options=[42.12], type="enumeration", base="decimal") - facet = ids.Property(propertySet="Foo_Bar", name="Foobar", value=restriction) - 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={"Foobar": 42.12}) - run("", facet=facet, inst=element, expected=True) - - # Measure may be used to specify an IFC data type - restriction = ids.Restriction(options=[2], type="enumeration", base="decimal") - facet = ids.Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Time") - 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.createIfcMassMeasure(2)}) - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcTimeMeasure(2)}) - run("", facet=facet, inst=element, expected=True) - - # Measure also implies that a unit matters, and so a conversion shall take place to SI units - restriction = ids.Restriction(options=[2], type="enumeration", base="decimal") - facet = ids.Property(propertySet="Foo_Bar", name="Foo", value=restriction, measure="Length") - 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("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2000)}) - run("", facet=facet, inst=element, expected=True) - - # The facet checks inherited properties from 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) - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - facet = ids.Property(propertySet="Foo_Bar", name="Foo") - run("", facet=facet, inst=wall, expected=True) - run("", facet=facet, inst=wall_type, expected=True) - - # The facet checks overriden properties from the occurrence - 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) - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) - pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar") - ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - facet = ids.Property(propertySet="Foo_Bar", name="Foo", value="Bar") - run("", facet=facet, inst=wall, expected=True) - run("", facet=facet, inst=wall_type, expected=False) - - def test_creating_a_material_facet(self): - facet = ids.Material() - assert facet.asdict() == {} - facet = ids.Material( - value="value", uri="https://test.com", minOccurs="0", maxOccurs="unbounded", instructions="instructions" - ) - assert facet.asdict() == { - "value": {"simpleValue": "value"}, - "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", - "@instructions": "instructions", - } - - def test_filtering_using_a_material_facet(self): - set_facet("material") - - facet = ids.Material() - ifc = ifcopenshell.file() - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - run("Elements without a material always fail", facet=facet, inst=element, expected=False) - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) - run("Elements with any material will pass an empty material facet", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - facet = ids.Material(value="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) - run("Material with no data will fail a value check", facet=facet, inst=element, expected=False) - material.Name = "Foo" - run("A material name may pass the value check", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("A material category may pass the value check", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - facet = ids.Material(value="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialList") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) - run("A material list with no data will fail a value check", facet=facet, inst=element, expected=False) - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.add_list_item", ifc, material_list=material_set, material=material) - material.Name = "Foo" - run("Any material Name in a list will pass a value check", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("Any material Category in a list will pass a value check", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - facet = ids.Material(value="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) - material = ifcopenshell.api.run("material.add_material", ifc) - layer = ifcopenshell.api.run("material.add_layer", ifc, layer_set=material_set, material=material) - layer.Name = "Foo" - run("Any layer Name in a layer set will pass a value check", facet=facet, inst=element, expected=True) - layer.Name = "Bar" - layer.Category = "Foo" - run("Any layer Category in a layer set will pass a value check", facet=facet, inst=element, expected=True) - layer.Category = "Bar" - material.Name = "Foo" - run("Any material Name in a layer set will pass a value check", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("Any material Category in a layer set will pass a value check", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - facet = ids.Material(value="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialProfileSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) - material = ifcopenshell.api.run("material.add_material", ifc) - profile = ifcopenshell.api.run("material.add_profile", ifc, profile_set=material_set, material=material) - profile.Name = "Foo" - profile.Profile = ifc.createIfcCircleProfileDef("AREA", None, None, 1) - run("Any profile Name in a profile set will pass a value check", facet=facet, inst=element, expected=True) - profile.Name = "Bar" - profile.Category = "Foo" - run("Any profile Category in a profile set will pass a value check", facet=facet, inst=element, expected=True) - profile.Category = "Bar" - material.Name = "Foo" - run("Any material Name in a profile set will pass a value check", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("Any material category in a profile set will pass a value check", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - facet = ids.Material(value="Foo") - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialConstituentSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) - run("A constituent set with no data will fail a value check", facet=facet, inst=element, expected=False) - material = ifcopenshell.api.run("material.add_material", ifc) - constituent = ifcopenshell.api.run( - "material.add_constituent", ifc, constituent_set=material_set, material=material - ) - constituent.Name = "Foo" - run("Any constituent Name in a constituent set will pass a value check", facet=facet, inst=element, expected=True) - constituent.Name = "Bar" - constituent.Category = "Foo" - run("Any constituent Category in a constituent set will pass a value check", facet=facet, inst=element, expected=True) - constituent.Category = "Bar" - material.Name = "Foo" - run("Any material Name in a constituent set will pass a value check", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("Any material Category in a constituent set will pass a value check", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) - material.Name = "Foo" - facet = ids.Material(value="Foo") - run("Occurrences can inherit materials from their types", facet=facet, inst=element, expected=True) - - ifc = ifcopenshell.file() - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) - material.Name = "Bar" - material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) - material.Name = "Foo" - facet = ids.Material(value="Foo") - run("Occurrences can override materials from their types", facet=facet, inst=element, expected=True) - - def test_creating_a_partof_facet(self): - facet = ids.PartOf() - assert facet.asdict() == {"@entity": "IfcSystem"} - facet = ids.PartOf(entity="IfcGroup") - assert facet.asdict() == {"@entity": "IfcGroup"} - - 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") - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) - facet = ids.PartOf(entity="IfcElementAssembly") - run("", facet=facet, inst=element, expected=False) - run("", 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 = ids.PartOf(entity="IfcElementAssembly") - run("", 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 = ids.PartOf(entity="IfcElementAssembly") - run("", 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 = ids.PartOf(entity="IfcGroup") - run("", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group) - run("", 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 = ids.PartOf(entity="IfcGroup") - ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group) - run("", 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 = ids.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) - - # 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 = ids.PartOf(entity="IfcSystem") - run("", facet=facet, inst=element, expected=True) - - """ Creating IDS with restrictions """ - - def test_create_restrictions_enumeration(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(ids.Entity(name="Test_Name")) - r = ids.Restriction(options=["testA", "testB"], type="enumeration") - m = ids.Material(value=r) - i.specifications[0].requirements.append(m) - self.assertEqual(i.specifications[0].requirements[0].value, "testA") - self.assertEqual(i.specifications[0].requirements[0].value, "testB") - self.assertNotEqual(i.specifications[0].requirements[0].value, "testC") - - def test_create_restrictions_bounds(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(ids.Entity(name="Test_Name")) - r = ids.Restriction(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") - p = ids.Property(propertySet="Test", name="Test", value=r) - i.specifications[0].requirements.append(p) - self.assertEqual(i.specifications[0].requirements[0].value, 0) - self.assertEqual(i.specifications[0].requirements[0].value, 5) - self.assertNotEqual(i.specifications[0].requirements[0].value, -1) - self.assertNotEqual(i.specifications[0].requirements[0].value, 10) - - def test_create_restrictions_pattern_simple(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(ids.Entity(name="Test_Name")) - r = ids.Restriction(options="[A-Z]{2,4}", type="pattern") - p = ids.Property(propertySet="Test", name="Test", value=r) - i.specifications[0].requirements.append(p) - self.assertEqual(i.specifications[0].requirements[0].value, "XYZ") - self.assertNotEqual(i.specifications[0].requirements[0].value, "abc") - self.assertNotEqual(i.specifications[0].requirements[0].value, "ABCDE") - self.assertNotEqual(i.specifications[0].requirements[0].value, "A") - - def test_create_restrictions_pattern_advanced(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(ids.Entity(name="Test_Name")) - r = ids.Restriction(options="(Wanddurchbruch|Deckendurchbruch).*", type="pattern") - p = ids.Property(propertySet="Test", name="Test", value=r) - i.specifications[0].requirements.append(p) - self.assertEqual(i.specifications[0].requirements[0].value, "Wanddurchbruch") - self.assertEqual(i.specifications[0].requirements[0].value, "Deckendurchbruch") - self.assertNotEqual(i.specifications[0].requirements[0].value, "Deeckendurchbruch") - - def test_create_restrictions_pattern_utf(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(ids.Entity(name="Test_Name")) - r = ids.Restriction(options="èêóòâôæøåążźćęóʑʒʓʔʕʗʘʙʚʛʜʝʞ", type="pattern") - p = ids.Property(propertySet="Test", name="Test", value=r) - i.specifications[0].requirements.append(p) - self.assertEqual(i.specifications[0].requirements[0].value, "èêóòâôæøåążźćęóʑʒʓʔʕʗʘʙʚʛʜʝʞ") - - def test_created_ids_to_xml(self): - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - e = ids.Entity(name="Test_Name", predefinedType="Test_PredefinedType") - c = ids.Classification(value="Test_Value", system="Test_System") - m = ids.Material(value="Test_Value") - re = ids.Restriction(options=["testA", "testB"], type="enumeration") - rb = ids.Restriction(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") - rp1 = ids.Restriction(options="[A-Z]{2,4}", type="pattern") - rp2 = ids.Restriction(options="èêóòâôæøåążźćęóʑʒʓʔʕʗʘʙʚʛʜʝʞ", type="pattern") - p1 = ids.Property(propertySet="Test_PropertySet", name="Test_Parameter", value=re) - p2 = ids.Property(propertySet="Test_PropertySet", name="Test_Parameter", value=rb) - p3 = ids.Property(propertySet="Test_PropertySet", name="Test_Parameter", value=rp1) - p4 = ids.Property(propertySet="Test_PropertySet", name="Test_Parameter", value=rp2) - p5 = ids.Property(propertySet="Test_PropertySet", name="Test_Parameter", value=[re, rb, rp1]) - i.specifications[0].applicability.append(e) - i.specifications[0].applicability.append(m) - i.specifications[0].requirements.append(c) - i.specifications[0].requirements.append(p1) - i.specifications[0].requirements.append(p2) - i.specifications[0].requirements.append(p3) - i.specifications[0].requirements.append(p4) - i.specifications[0].requirements.append(p5) - fn = "TEST_FILE.xml" - result = i.to_xml(fn) + def test_saving_to_xml(self): + specs = ids.Ids(title="Title") + spec = ids.Specification(name="Name") + spec.applicability.append(ids.Entity(name="IFCWALL")) + spec.requirements.append(ids.Attribute(name="Name", value="Waldo")) + specs.specifications.append(spec) + fn = "tmp.xml" + result = specs.to_xml(fn) os.remove(fn) - self.assertTrue(result) - -class TestIfcValidation(unittest.TestCase): def test_creating_a_minimal_ids_and_validating(self): specs = ids.Ids(title="Title") spec = ids.Specification(name="Name") @@ -1269,117 +134,35 @@ class TestIfcValidation(unittest.TestCase): assert spec.requirements[0].failed_entities == [wall] assert spec2.requirements[0].failed_entities == [wall] - def test_validate_simple(self): - return # TODO - # Same test as in reporting... - ids_file = ids.open(IDS_URL) - report = ids.SimpleHandler() - logger.addHandler(report) - ids_file.validate(ifc_file) - self.assertEqual(len(report.statements), 5) - logger.handlers.pop() - def test_validate_all_facets(self): - # Those are true: - e = ids.Entity(name="IfcWall") - p1 = ids.Property(propertySet="MySet", name="Param1", value="banan") - p2 = ids.Property(propertySet="MySet", name="Param2", value=120.0) - p3 = ids.Property(propertySet="Pset_WallCommon", name="LoadBearing", value=False) - # Those are false: - p4 = ids.Property(propertySet="MySet", name="Param1", value="orange") - p5 = ids.Property(propertySet="MySet", name="Param2", value=123.4) - p6 = ids.Property(propertySet="Pset_WallCommon", name="LoadBearing", value=True) +class TestSpecification: + def test_create_specification_with_minimal_information(self): + spec = ids.Specification() + assert spec.asdict() == { + "@name": "Unnamed", + "@ifcVersion": ["IFC2X3", "IFC4"], + "applicability": {}, + "requirements": {}, + } - i = ids.Ids(title="My IDS") - i.specifications.append(ids.Specification(name="Test_Specification")) - i.specifications[0].applicability.append(e) - i.specifications[0].requirements.append(p1) - i.specifications[0].requirements.append(p2) - i.specifications[0].requirements.append(p3) - i.specifications[0].requirements.append(p4) - i.specifications[0].requirements.append(p5) - i.specifications[0].requirements.append(p6) - - report = ids.SimpleHandler(report_valid=True) - logger.addHandler(report) - - i.validate(ifc_file) - # TODO self.assertEqual(len(report.statements), 27) #there are 5 walls in the IFC, one passed 3 criteria, est should fail (27 failures) - logger.handlers.pop() - - """ Validating IDS files with restrictions """ - - def test_validate_restrictions_enumeration(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), - "Sample-BIM-Files/IDS/", - "IDS_Wall_needs_property_with_restriction_enumeration.xml", + def test_create_specification_with_all_possible_information(self): + spec = ids.Specification( + name="name", + minOccurs="0", + maxOccurs="unbounded", + ifcVersion="IFC4", + identifier="identifier", + description="description", + instructions="instructions", ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - [ - x["@value"] - for x in ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["enumeration"] - ], - ["testA", "testB"], - ) - # TODO actual test of validation result - # self.assertTrue( ) - - def test_validate_restrictions_boundsInclusive(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_property_with_restriction_bounds.xml" - ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["minInclusive"]["@value"], - "0", - ) - # TODO actual test of validation result - # self.assertTrue( ) - - def test_validate_restrictions_boundsExclusive(self): - # TODO - pass - - def test_validate_restrictions_pattern_simple(self): - return # TODO - IDS_URL = os.path.join( - os.path.dirname(__file__), "Sample-BIM-Files/IDS/", "IDS_Wall_needs_property_with_restriction_pattern.xml" - ) - ids_file = ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements[0].node["value"]["restriction"][0]["pattern"]["@value"], - "[A-Z]{2,4}", - ) - # TODO actual test of validation result - # self.assertTrue( ) - - -class TestIdsReporting(unittest.TestCase): - def test_simple_report(self): - return # TODO - # Same test as in validation... - ids_file = ids.open(IDS_URL) - report = ids.SimpleHandler() - logger.addHandler(report) - ids_file.validate(ifc_file) - self.assertEqual(len(report.statements), 5) - logger.handlers.pop() - - def test_bcf_report(self): - return # TODO - ids_file = ids.open(IDS_URL) - fn = os.path.join(tempfile.gettempdir(), "test.bcf") - bcf_handler = ids.BcfHandler(project_name="Default IDS Project", author="your@email.com", filepath=fn) - logger.addHandler(bcf_handler) - ids_file.validate(ifc_file) - my_bcfxml = bcfxml.load(fn) - topics = my_bcfxml.get_topics() - self.assertEqual(len(topics), 5) - logger.handlers.pop() + assert spec.asdict() == { + "@name": "name", + "@minOccurs": "0", + "@maxOccurs": "unbounded", + "@ifcVersion": "IFC4", + "@identifier": "identifier", + "@description": "description", + "@instructions": "instructions", + "applicability": {}, + "requirements": {}, + }