diff --git a/src/ifcopenshell-python/ifcopenshell/ids.py b/src/ifcopenshell-python/ifcopenshell/ids.py deleted file mode 100644 index 9d7082541d..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/ids.py +++ /dev/null @@ -1,1499 +0,0 @@ -# IDS - Information Delivery Specification. -# Copyright (C) 2021 Artur Tomczak , Thomas Krijnen , Dion Moult -# -# This file is part of IfcOpenShell. -# -# IfcOpenShell 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, -# 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 . - -import os -import re -import logging -import numpy as np -import datetime -import builtins -import ifcopenshell.util.unit -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.classification -from bcf.v2.bcfxml import BcfXml -from bcf.v2 import data as bcf -from xmlschema import XMLSchema -from xmlschema import etree_tostring -from xmlschema.validators import identities -from xml.etree import ElementTree as ET - - -# http://standards.buildingsmart.org/IDS/ids_05.xsd -cwd = os.path.dirname(os.path.realpath(__file__)) -ids_schema = XMLSchema(os.path.join(cwd, "ids.xsd")) - - -def error(msg): - raise Exception(msg) - - -class ids: - """Represents the XML root node and its childNodes.""" - - def __init__( - self, - title="Untitled", - copyright=None, - version=None, - description=None, - author=None, - date=None, - purpose=None, - milestone=None, - ): - """Create an IDS object. - - :param title: Name of the IDS file, defaults to None - :type title: str, required - :param copyright:, defaults to None - :type copyright: str, optional - :param version: IDS file version, defaults to None - :type version: float, optional - :param description:, defaults to None - :type description: str, optional - :param author: Email of the IDS author, defaults to None - :type author: str, optional - :param date: Date in 'yyyy-mm-dd' format, defaults to current date - :type date: str, optional - :param purpose:, defaults to None - :type purpose: str, optional - :param milestone:, defaults to None - :type milestone: str, optional - """ - self.specifications = [] - self.info = {} - self.info["title"] = title or "Untitled" - if copyright: - self.info["copyright"] = copyright - if version: - self.info["version"] = version - if description: - self.info["description"] = description - if author and "@" in author: - self.info["author"] = author - if date: - try: - self.info["date"] = datetime.date.fromisoformat(date).isoformat() - except ValueError: - pass - if purpose: - self.info["purpose"] = purpose - if milestone: - self.info["milestone"] = milestone - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - ids_dict = { - "@xmlns": "http://standards.buildingsmart.org/IDS", - "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", - "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", - "info": self.info, - "specifications": {"specification": []}, - } - for spec in self.specifications: - ids_dict["specifications"]["specification"].append(spec.asdict()) - return ids_dict - - def to_string(self, ids_schema=ids_schema): - """Convert IDS object to XML string - - :param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART - :type ids_schema: XMLschema, optional - :return: The contents of the XML data in string form - :rtype: string - """ - ns = {"": "http://standards.buildingsmart.org/IDS"} - return etree_tostring(ids_schema.encode(self.asdict()), namespaces=ns) - - def to_xml(self, filepath="output.xml", ids_schema=ids_schema): - """Writes IDS object to an XML file. - - :param filepath: Path to the file, defaults to "output.xml" - :type filepath: str, optional - :param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART - :type ids_schema: XMLschema, optional - :return: Result of the newly created file validation against the schema. - :rtype: bool - """ - ET.register_namespace("", "http://standards.buildingsmart.org/IDS") - ET.ElementTree(ids_schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) - return ids_schema.is_valid(filepath) - - @staticmethod - def open(filepath, ids_schema=ids_schema): - """Use to open ids.xml files - - :param filepath: ids file path - :type filepath: str - :param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART - :type ids_schema: XMLschema, optional - :return: IDS file as a python object - :rtype: ids object - """ - - ids_schema.validate(filepath) - ids_content = ids_schema.decode( - filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"} - ) - ids_file = ids() - ids_file.specifications = [specification.parse(s) for s in ids_content["specifications"]["specification"]] - return ids_file - - def validate2(self, ifc_file): - """Use to validate IFC model against IDS specifications. - - :param ifc_file: path to ifc file - :type ifc_file: str - :param logger: Logging object with handlers, defaults to None - :type logger: logging, optional - """ - for specification in self.specifications: - specification.applicable_entities.clear() - specification.failed_entities.clear() - specification.status = None - for element in ifc_file: - for specification in self.specifications: - if not specification.applicability(element, None): - continue - specification.applicable_entities.append(element) - if specification.requirements(element, None): - specification.status = True - else: - specification.status = False - specification.failed_entities.append(element) - - def validate(self, ifc_file, logger=None): - """Use to validate IFC model against IDS specifications. - - :param ifc_file: path to ifc file - :type ifc_file: str - :param logger: Logging object with handlers, defaults to None - :type logger: logging, optional - """ - if not isinstance(logger, logging.Logger): - logger = logging.getLogger("IDS_Logger") - logging.basicConfig(level=logging.INFO, format="%(message)s") - logger.setLevel(logging.INFO) - - # Consider other way around: for elem, for spec so we can see if an element pass all IDSes? - for spec in self.specifications: - self.ifc_applicable = 0 - self.ifc_passed = 0 - for elem in ifc_file: - apply, comply = spec(elem, logger) - if apply: - self.ifc_applicable += 1 - if comply: - self.ifc_passed += 1 - if self.ifc_applicable == 0: - if spec.minOccurs != "0": - logger.error("No applicable elements found. Minimum 1 applicable element required.") - else: - logger.debug("No applicable elements found. None required.") - - try: - percentage = self.ifc_passed / self.ifc_applicable * 100 - except ZeroDivisionError: - percentage = 0 - - logger.debug( - "Out of %s IFC elements, %s were applicable and %s of them passed (%s)." - % ( - len(ifc_file.by_type("IfcProduct")), - self.ifc_applicable, - self.ifc_passed, - str(percentage) + "%", - ) - ) - for h in logger.handlers: - h.flush() - - -class specification: - """Represents the XML node and its two children and """ - - def __init__( - self, - name="Unnamed", - minOccurs=None, - maxOccurs=None, - ifcVersion=["IFC2X3", "IFC4"], - identifier=None, - description=None, - instructions=None, - ): - """Create a specification to be added in ids. - - :param name: Name describing the specification to a contract reader - :type name: str - :param minOccurs: The minimum total entities that should pass as an integer >= 0 - :type minOccurs: str, optional - :param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded" - :type maxOccurs: str, optional - """ - self.name = name or "Unnamed" - self.applicability = None - self.requirements = None - self.minOccurs = minOccurs - self.maxOccurs = maxOccurs - self.ifcVersion = ifcVersion - self.identifier = identifier - self.description = description - self.instructions = instructions - - self.applicable_entities = [] - self.failed_entities = [] - self.status = None - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - # if older python collections.OrderedDict() - results = { - "@name": self.name, - "@ifcVersion": self.ifcVersion, - "applicability": {}, - "requirements": {}, - } - for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]: - value = getattr(self, attribute) - if value: - results[f"@{attribute}"] = value - for clause_type in ["applicability", "requirements"]: - clause = getattr(self, clause_type) - if not clause: - continue - for fac in clause.terms: - fclass = type(fac).__name__ - if fclass in results[clause_type]: - results[clause_type][fclass].append(fac.asdict()) - else: - results[clause_type][fclass] = [fac.asdict()] - return results - - @staticmethod - def parse(ids_dict): - """Parse xml specification to python object. - - :param ids_dict: - :type ids_dict: dict - """ - - def parse_rules(dict): - facet_names = list(dict.keys()) - facet_properties = [v[0] if isinstance(v, list) else v for v in list(dict.values())] - classes = [meta_facet.facets.__getitem__(f) for f in facet_names] - facets = [cls(n) for cls, n in zip(classes, facet_properties)] - return facets - - spec = specification() - try: - spec.name = ids_dict["@name"] - except KeyError: - spec.name = "" - spec.minOccurs = ids_dict["@minOccurs"] - spec.maxOccurs = ids_dict["@maxOccurs"] - spec.ifcVersion = ids_dict["@ifcVersion"] - spec.applicability = boolean_and(parse_rules(ids_dict["applicability"])) - spec.requirements = boolean_and(parse_rules(ids_dict["requirements"])) - return spec - - def add_applicability(self, facet): - """Applicability specifies a filter for IFC entities are to be validated. - - At least one filter must be added. - - :param facet: any of entity|attribute|classification|property|material - :type facet: facet - - Example:: - - specs = ids.ids() - spec = ids.specification(name="Test_Specification") - spec.add_applicability(ids.entity.create(name="IfcWall")) - specs.specifications.append(spec) - """ - if self.applicability: - self.applicability = boolean_and(self.applicability.terms + [facet]) - else: - self.applicability = boolean_and([facet]) - - def add_requirement(self, facet): - """A requirement specifies data to be checked for all applicable entities. - - At least one requirement must be added. - - :param facet: any of entity|attribute|classification|property|material|partOf - :type facet: facet - """ - if self.requirements: - self.requirements = boolean_and(self.requirements.terms + [facet]) - else: - self.requirements = boolean_and([facet]) - - def __call__(self, inst, logger): - """When specification is called on an ifc instance, it validates against applicability and requirements. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: results of validation on applicability and requirements - :rtype: [bool,bool] - """ - if self.applicability(inst, logger): - valid = self.requirements(inst, logger) - - if valid: - logger.info( - { - "guid": inst.GlobalId, - "result": valid.success, - "sentence": str(self) - + ".\n" - + inst.is_a() - + " '" - + str(inst.Name) - + "' (#" - + str(inst.id()) - + ") has " - + str(valid) - + " so is compliant", - "ifc_element": inst, - } - ) - return True, True - else: - # BUG "has does not have" - logger.error( - { - "guid": inst.GlobalId, - "result": valid.success, - "sentence": str(self) - + ".\n" - + inst.is_a() - + " '" - + str(inst.Name) - + "' (#" - + str(inst.id()) - + ") has " - + str(valid) - + " so is not compliant", - "ifc_element": inst, - } - ) - return True, False - else: - return False, False - - def __str__(self): - """Represent the specification in human readable sentence. - - :return: sentence - :rtype: str - """ - return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ - - -class facet_evaluation: - """The evaluation of a facet with data from IFC. Converts to bool and has a human readable string format.""" - - def __init__(self, success, str): - self.success = success - self.str = str - - def __bool__(self): - return self.success - - def __str__(self): - return self.str - - -class meta_facet(type): - """A metaclass for automatically registering facets in a map to be instantiated based on XML tagnames.""" - - facets = {} - - def __new__(cls, clsname, bases, attrs): - newclass = super(meta_facet, cls).__new__(cls, clsname, bases, attrs) - meta_facet.facets[clsname] = newclass - return newclass - - -class facet(metaclass=meta_facet): - """ - The base class for IDS facets. IDS facets are functors constructed from - XML nodes that return True or False. A getattr method is provided for - conveniently extracting XML child node text content. - Use child classes instead: entity, classification, property and material. - """ - - def __init__(self, node=None): - if node: - self.node = node - - def __getattr__(self, attr): - - if attr in getattr(self, "node", None): - v = self.node[attr] - - # BUG list of dictionaries should not happen - if isinstance(v, list): - v = v[0] - - if "simpleValue" in list(v): - return v["simpleValue"] - elif "restriction" in list(v): - return restriction.parse(v["restriction"][0]) - # TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]] - else: - raise Exception("Unknown value declaration.") - # except KeyError: - else: - return None - - def __iter__(self): - for k in self.parameters: - yield k, getattr(self, k) - - def __str__(self): - di = dict(list(self)) - for k, v in di.items(): - if isinstance(v, str) and not len(v): - di[k] = "not specified" - return self.message % di - - -class entity(facet): - """The IDS entity facet currently *with* inheritance""" - - parameters = ["name", "predefinedType", "instructions"] - - @staticmethod - def create(name=None, predefinedType=None, instructions=None): - """Create an entity facet that can be added to applicability or requirements of IDS specification. - - :param name: IFC entity name that is required. e.g. IfcWall, defaults to None - :type name: str, optional - :param predefinedType: name of the predefined type, defaults to None - :type predefinedType: str, optional - :return: entity object - :rtype: entity - """ - - inst = entity() - inst.name = name - inst.predefinedType = predefinedType - inst.instructions = instructions - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - results = {"name": parameter_asdict(self.name)} - if self.predefinedType: - results["predefinedType"] = parameter_asdict(self.predefinedType) - if self.instructions: - results["@instructions"] = self.instructions - return results - - def __call__(self, inst, logger=None): - """Validate an entity. - - Subclasses are not considered to pass the requirements. PredefinedType - checks support userdefined types for both element and type elements. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - is_pass = inst.is_a().upper() == self.name - if is_pass and self.predefinedType: - predefined_type = ifcopenshell.util.element.get_predefined_type(inst) - is_pass = predefined_type == self.predefinedType - - if self.predefinedType: - self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'" - return facet_evaluation(is_pass, self.message % {"name": inst.is_a(), "predefinedType": predefined_type}) - else: - self.message = "an entity name '%(name)s'" - return facet_evaluation(is_pass, self.message % {"name": inst.is_a()}) - - -class attribute(facet): - """The IDS attribute facet""" - - parameters = ["name", "value", "minOccurs", "maxOccurs", "instructions"] - - @staticmethod - def create(name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None): - """Create an attribute facet that can be added to applicability or requirements of IDS specification. - - :param name: Attribute name, such as "Description" - :type name: str - :param value: Attribute value, with type being strictly checked - :type value: str, optional - :param minOccurs: The minimum total entities that should pass as an integer >= 0 - :type minOccurs: str, optional - :param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded" - :type maxOccurs: str, optional - :param instructions: Instructions as a guide for model authors when reading the requirements - :type instructions: str, optional - :return: entity object - :rtype: entity - """ - - inst = attribute() - inst.name = name - inst.value = value - inst.minOccurs = minOccurs - inst.maxOccurs = maxOccurs - inst.instructions = instructions - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - results = {"name": parameter_asdict(self.name)} - if self.value: - results["value"] = parameter_asdict(self.value) - if self.minOccurs: - results["@minOccurs"] = self.minOccurs - if self.maxOccurs: - results["@maxOccurs"] = self.maxOccurs - if self.instructions: - results["@instructions"] = self.instructions - return results - - def __call__(self, inst, logger=None): - """Validate an ifc instance. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - - def get_values(element, name): - if isinstance(name, str): - return [getattr(element, name, None)] - 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) - - if is_pass: - for i, value in enumerate(values): - if value is None: - is_pass = False - elif value == "": - is_pass = False - elif value == tuple(): - is_pass = False - 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 - except: - if names[i] in inst.wrapped_data.get_inverse_attribute_names(): - is_pass = False - if not is_pass: - break - - if is_pass and self.value: - for value in values: - if isinstance(value, ifcopenshell.entity_instance): - is_pass = False - break - elif isinstance(self.value, str) and isinstance(value, str): - if value != self.value: - is_pass = False - break - elif isinstance(self.value, str): - cast_value = cast_to_value(self.value, value) - if value != cast_value: - is_pass = False - break - elif value != self.value: - is_pass = False - break - - if self.value: - self.message = "foo" - return facet_evaluation(is_pass, f"an entity with {self.name} set to something wrong") - else: - return facet_evaluation(is_pass, f"an entity with {self.name}") - - -class classification(facet): - """ - The IDS classification facet by traversing the HasAssociations inverse attribute - """ - - parameters = ["system", "value", "uri", "minOccurs", "maxOccurs" "instructions"] - message = "sclassification reference %(value)s from '%(system)s'" - - @staticmethod - def create(value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): - """Create a classification facet that can be added to applicability or requirements of IDS specification. - - :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None - :type value: restriction|alphanumeric, optional - :param system: System that is required. Could be alphanumeric or restriction object, defaults to None - :type system: restriction|alphanumeric, optional - :param minOccurs: The minimum total entities that should pass as an integer >= 0 - :type minOccurs: str, optional - :param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded" - :type maxOccurs: str, optional - :return: classification object - :rtype: classification - """ - inst = classification() - inst.value = value - inst.system = system - inst.uri = uri - inst.minOccurs = minOccurs - inst.maxOccurs = maxOccurs - inst.instructions = instructions - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - results = {} - if self.value: - results["value"] = parameter_asdict(self.value) - if self.system: - results["system"] = parameter_asdict(self.system) - if self.uri: - results["@uri"] = self.uri - if self.minOccurs: - results["@minOccurs"] = self.minOccurs - if self.maxOccurs: - results["@maxOccurs"] = self.maxOccurs - if self.instructions: - results["@instructions"] = self.instructions - return results - - def __call__(self, inst, logger=None): - """Validate an ifc instance against that classification facet. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - 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) - if is_pass and self.value: - is_pass = any( - [self.value == getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references] - ) - if is_pass and self.system: - is_pass = any( - [self.system == ifcopenshell.util.classification.get_classification(r).Name for r in references] - ) - - if references: - return facet_evaluation( - is_pass, - self.message - % { - "system": list(references)[0][0], - "value": list(references)[0][1], - }, # TODO Fix this 0 index reference assumption when I refactor out the messages - ) - else: - return facet_evaluation(False, "does not have classification reference") - - -class partOf(facet): - """ - The IDS partOf facet by traversing the _______ inverse attribute - """ - - parameters = ["entity"] - message = "relation as part of %(entity)s" - - @staticmethod - def create(entity="IfcSystem"): - """Create a partOf facet that can be added to applicability or requirements of IDS specification. - - :param entity: Entity that should contain this object. Could be alphanumeric or restriction object, defaults to None - :type entity: restriction|alphanumeric, optional - :return: partOf object - :rtype: partOf - """ - - inst = partOf() - inst.entity = entity - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - return {"@entity": self.entity} - - def __call__(self, inst, logger=None): - """Validate an ifc instance against that partOf facet. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - 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 facet_evaluation(is_pass, "is not a part of") - - -class property(facet): - """ - The IDS property facet implemented using `ifcopenshell.util.element` - """ - - parameters = ["name", "propertySet", "value"] - message = "property '%(name)s' in '%(propertySet)s' with a value %(value)s" - - @staticmethod - def create( - propertySet="Property_Set", - name="PropertyName", - value=None, - measure=None, - uri=None, - minOccurs=None, - maxOccurs=None, - instructions=None, - ): - """Create a property facet that can be added to applicability or requirements of IDS specification. - - :param propertySet: Propertyset that is required. Could be alphanumeric or restriction object, defaults to None - :type propertySet: restriction|alphanumeric, optional - :param name: Name that is required. Could be alphanumeric or restriction object, defaults to None - :type name: restriction|alphanumeric, optional - :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None - :type value: restriction|alphanumeric, optional - :param minOccurs: The minimum total entities that should pass as an integer >= 0 - :type minOccurs: str, optional - :param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded" - :type maxOccurs: str, optional - :return: property object - :rtype: property - """ - inst = property() - inst.propertySet = propertySet - inst.name = name - inst.value = value - inst.measure = measure - inst.uri = uri - inst.minOccurs = minOccurs - inst.maxOccurs = maxOccurs - inst.instructions = instructions - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - results = { - "propertySet": parameter_asdict(self.propertySet), - "name": parameter_asdict(self.name), - } - if self.value: - results["value"] = parameter_asdict(self.value) - if self.measure: - results["@measure"] = self.measure - if self.uri: - results["@uri"] = self.uri - if self.minOccurs: - results["@minOccurs"] = self.minOccurs - if self.maxOccurs: - results["@maxOccurs"] = self.maxOccurs - if self.instructions: - results["@instructions"] = self.instructions - # TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something - return results - - def __call__(self, inst, logger=None): - """Validate an ifc instance against that property facet. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - 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 - - # TODO implement data type comparison - # xs:string - # xs:decimal - # xs:integer - # xs:boolean - # xs:anyURI - # xs:date YYYY-MM-DD - # xs:time hh:mm:ss - # xs:dateTime YYYY-MM-DDThh:mm:ss - # xs:duration PnYnMnDTnHnMnS - - return facet_evaluation(is_pass, "todo") - - -class material(facet): - """The IDS material facet used to traverse the HasAssociations inverse attribute.""" - - parameters = ["value"] - message = "material '%(value)s'" - - @staticmethod - def create(value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None): - """Create a material facet that can be added to applicability or requirements of IDS specification. - - :param value: Value that is required. Could be alphanumeric or restriction object, defaults to None - :type value: restriction|alphanumeric, optional - :return: material object - :rtype: material - """ - inst = material() - inst.value = value - inst.uri = uri - inst.minOccurs = minOccurs - inst.maxOccurs = maxOccurs - inst.instructions = instructions - return inst - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - results = {} - if self.value: - results["value"] = parameter_asdict(self.value) - if self.uri: - results["@uri"] = self.uri - if self.minOccurs: - results["@minOccurs"] = self.minOccurs - if self.maxOccurs: - results["@maxOccurs"] = self.maxOccurs - if self.instructions: - results["@instructions"] = self.instructions - return results - - def __call__(self, inst, logger=None): - """Validate an ifc instance against that material facet. - - :param inst: IFC entity element - :type inst: IFC entity - :param logger: Logging object - :type logger: logging - :return: result of the validation as bool and message - :rtype: facet_evaluation(bool, str) - """ - material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True) - - is_pass = material is not None - - 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 - - return facet_evaluation( - is_pass, - self.message % {"value": "todo", "location": "todo"}, - ) - - -def parameter_asdict(parameter): - """Converts parameter to an IDS compliant dictionary, handling both value and restrictions. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - 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 boolean_logic: - """Boolean conjunction over a collection of functions""" - - def __init__(self, terms): - self.terms = terms - - def __call__(self, *args): - eval = [t(*args) for t in self.terms] - join = [" and ", " or "][self.fold == any] - return facet_evaluation(self.fold(eval), join.join(map(str, eval))) - - def __str__(self): - return [" and ", " or "][self.fold == any].join(map(str, self.terms)) - - -class boolean_and(boolean_logic): - fold = all - - -class boolean_or(boolean_logic): - fold = any - - -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: - """ - The value restriction from XSD implemented as a list of values and a containment test - """ - - def __init__(self): - """Create a restriction that can be used instead of value of a parameter.""" - self.type = "" - self.options = [] - - @staticmethod - def parse(ids_dict): - """Parse xml restriction to python object. - - :param ids_dict: - :type ids_dict: dict - """ - r = restriction() - if ids_dict: - # TODO 'base' missing in some IDS?! - - try: - r.base = ids_dict["@base"][3:] - except KeyError: - r.base = "String" - - for n in ids_dict: - if n == "enumeration": - r.type = "enumeration" - for x in ids_dict[n]: - r.options.append(x["@value"]) - elif n[-7:] == "clusive": - r.type = "bounds" - r.options.append({n: ids_dict[n]["@value"]}) - elif n[-5:] == "ength": - r.type = "length" - if n[3:6] == "min": - r.options.append(">=") - elif n[3:6] == "max": - r.options.append("<=") - else: - r.options.append("==") - r.options[-1] += str(ids_dict[n]["@value"]) - elif n == "pattern": - r.type = "pattern" - r.options.append(ids_dict[n]["@value"]) - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - elif n == "@base": - pass - else: - print("Error! Restriction not implemented") - return r - - def asdict(self): - """Converts object to a dictionary, adding required attributes. - - :return: Xmlschema compliant dictionary. - :rtype: dict - """ - 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 - - @staticmethod - def create(options, type="pattern", base="string"): - """Create restriction instead of simpleValue - - :param type: One of "enumeration"|"pattern"|"bounds", defaults to "pattern" - :type type: str, optional - :param options: if enumeration: list of possible values - if pattern: xml regular expression string - if bounds: dictionary with possible keys: 'minInclusive', 'maxInclusive', 'minExclusive', 'maxExclusive' - :type options: list|str|dict - :param base: One of "string"|"boolean"|"decimal"|"integer", defaults to "string" - :type base: str, optional - :raises Exception: If not properly defined restriction. - :return: restriction object - :rtype: restriction - """ - rest = restriction() - if type in ["enumeration", "pattern", "bounds"]: - rest.type = type - rest.base = base - rest.options = options - if ( - (type == "enumeration" and isinstance(options, list)) - or (type == "bounds" and isinstance(options, dict)) - or (type == "pattern" and isinstance(options, str)) - ): - rest.options = options - else: - raise Exception("Options were not properly defined.") - return rest - else: - raise Exception( - "Such restriction not implemented. Try: 'enumeration', 'pattern' or 'min/maxInclusive' or 'min/maxExclusive'." - ) - - def __eq__(self, other): - """Evaluate the restriction using equality sign. - - :param other: value to compare with the restriction. - :type other: str|float|int - :return: True if 'other' match the restriction, False if not. - :rtype: bool - """ - 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 __repr__(self): - """Represent the restriction in human readable sentence. - - :return: sentence - :rtype: str - """ - msg = "of type '%s', " % (self.base) - if self.type == "enumeration": - msg = msg + "of value: '%s'" % "' or '".join(self.options) - elif self.type == "bounds": - msg = msg + "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options]) - elif self.type == "length": - msg = msg + "with %s letters" % " and ".join(self.options) - elif self.type == "pattern": - msg = msg + "respecting the pattern '%s'" % self.options - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - return msg - - -class SimpleHandler(logging.StreamHandler): - """Logging handler listing all cases in python list.""" - - def __init__(self, report_valid=False): - """Logging handler listing all cases in python list. - - :param report_valid: True if you want to list all the compliant cases as well, defaults to False - :type report_valid: bool, optional - """ - logging.StreamHandler.__init__(self) - self.statements = [] - if report_valid: - self.setLevel(logging.DEBUG) - else: - self.setLevel(logging.ERROR) - - def emit(self, mymsg): - """Triggered on each use of logging with the Simple handler enabled. - - :param log_content: default logger message - :type log_content: string|dict - """ - self.statements.append(mymsg.msg) - - -class CsvHandler(logging.StreamHandler): - """Logging handler listing all cases in csv file.""" - - def __init__(self, filepath="./Report.csv", report_valid=False): - """Logging handler listing all cases in csv file. - - :param report_valid: True if you want to list all the compliant cases as well, defaults to False - :type report_valid: bool, optional - """ - import csv - - logging.StreamHandler.__init__(self) - if report_valid: - self.setLevel(logging.INFO) - else: - self.setLevel(logging.ERROR) - self.file = open(filepath, "w", encoding="UTF8", newline="") - self.csvwriter = csv.writer(self.file) - self.csvwriter.writerow(["guid", "result", "sentence"]) # header - - def emit(self, mymsg): - """Triggered on each use of logging with the Simple handler enabled. - - :param log_content: default logger message - :type log_content: string|dict - """ - # BUG bytes-like object is required, not 'str' - self.csvwriter.writerow(mymsg.msg) - - def flush(self): - self.file.close() - - -class BcfHandler(logging.StreamHandler): - """Logging handler for creation of BCF report files. - - :param project_name: defaults to "IDS Project" - :type project_name: str, optional - :param author: Email of the person creating the BCF report, defaults to "your@email.com" - :type author: str, optional - :param filepath: Path to save the BCF report, defaults to None - :type filepath: str, optional - :param report_valid: True if you want to list all the compliant cases as well, defaults to False - :type report_valid: bool, optional - - Example:: - - bcf_handler = BcfHandler( - project_name="Default IDS Project", - author="your@email.com", - filepath="example.bcf", - ) - logger = logging.getLogger("IDS_Logger") - logging.basicConfig(level=logging.INFO, format="%(message)s") - logger.addHandler(bcf_handler) - """ - - def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False): - - logging.StreamHandler.__init__(self) - if report_valid: - self.setLevel(logging.INFO) - else: - self.setLevel(logging.ERROR) - self.bcf = BcfXml() - self.bcf.author = author - self.bcf.new_project() - self.bcf.project.name = project_name - self.filepath = filepath - self.bcf.edit_project() - - def emit(self, log_content): - """Triggered on each use of logging with the BCF handler enabled. - - :param log_content: default logger message - :type log_content: string|dict - """ - topic = bcf.Topic() - topic.title = log_content.msg["sentence"].split(".\n")[1] - topic.description = log_content.msg["sentence"].split(".\n")[0] - self.bcf.add_topic(topic) - # try: # Add viewpoint and link to ifc object - viewpoint = bcf.Viewpoint() - viewpoint.perspective_camera = bcf.PerspectiveCamera() - ifc_elem = log_content.msg["ifc_element"] - # ifc_elem = ifc_file.by_guid(log_content.msg["guid"]) - target_position = np.array(ifcopenshell.util.placement.get_local_placement(ifc_elem.ObjectPlacement)) - target_position = target_position[:, 3][0:3] - camera_position = target_position + np.array((5, 5, 5)) - viewpoint.perspective_camera.camera_view_point.x = camera_position[0] - viewpoint.perspective_camera.camera_view_point.y = camera_position[1] - viewpoint.perspective_camera.camera_view_point.z = camera_position[2] - camera_direction = camera_position - target_position - camera_direction = camera_direction / np.linalg.norm(camera_direction) - camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) - camera_right = camera_right / np.linalg.norm(camera_right) - camera_up = np.cross(camera_direction, camera_right) - camera_up = camera_up / np.linalg.norm(camera_up) - rotation_transform = np.zeros((4, 4)) - rotation_transform[0, :3] = camera_right - rotation_transform[1, :3] = camera_up - rotation_transform[2, :3] = camera_direction - rotation_transform[-1, -1] = 1 - translation_transform = np.eye(4) - translation_transform[:3, -1] = -camera_position - look_at_transform = np.matmul(rotation_transform, translation_transform) - mat = np.linalg.inv(look_at_transform) - viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 - viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 - viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 - viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] - viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] - viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] - viewpoint.components = bcf.Components() - c = bcf.Component() - c.ifc_guid = log_content.msg["guid"] - viewpoint.components.selection.append(c) - viewpoint.components.visibility = bcf.ComponentVisibility() - viewpoint.components.visibility.default_visibility = True - viewpoint.snapshot = None - self.bcf.add_viewpoint(topic, viewpoint) - - def flush(self): - """Saves the BCF report to file. Triggered at the end of the validation process.""" - if not self.filepath: - self.filepath = os.getcwd() + r"\IDS_report.bcf" - if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")): - self.filepath = self.filepath + r"\IDS_report.bcf" - self.bcf.save_project(self.filepath) - - -location = {"instance": "an instance ", "type": "a type ", "any": "a "} - -bounds = { - "minInclusive": "larger or equal ", - "maxInclusive": "smaller or equal ", - "minExclusive": "larger than ", - "maxExclusive": "smaller than ", -} - -if __name__ == "__main__": - import sys, os - import ifcopenshell - - ids_file = ids.open(sys.argv[1]) - ifc_file = ifcopenshell.open(sys.argv[2]) - filepath = sys.argv[3] - - logger = logging.getLogger("IDS_Logger") - logging.basicConfig(filename=filepath, level=logging.INFO, format="%(message)s") - logging.FileHandler(filepath + r"\report.txt", mode="w") - - bcf_handler = BcfHandler( - project_name="Default IDS Project", - author="your@email.com", - filepath=filepath + r"\report.bcfzip", - ) - logger.addHandler(bcf_handler) - - report = SimpleHandler() - logger.addHandler(report) - - ids_file.validate(ifc_file, logger) diff --git a/src/ifcopenshell-python/ifcopenshell/ids.xsd b/src/ifcopenshell-python/ifcopenshell/ids.xsd deleted file mode 100644 index 362e401ffa..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/ids.xsd +++ /dev/null @@ -1,289 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - See the documentation and default units of these measures on https://github.com/buildingSMART/IDS/blob/master/Documentation/Physical_Quantities_and_Units.md - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Make sure 'Name' value of requirements entity is the same as the 'applicability' node, or a wildcard (inclusive pattern). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. - - - - - - - - - - - - - - Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. - - - - - - - - - - - - - - - Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. - - - - - - - - - - - - - - - Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Author of the IDS can provide an identifier to the IDS. Beware: this cannot be enforced/assumed as (global) unique. - - - - - - Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. - - - - - - - - - \ No newline at end of file diff --git a/src/ifcopenshell-python/test/ids_doc_generator.py b/src/ifcopenshell-python/test/ids_doc_generator.py deleted file mode 100644 index a703d4d48b..0000000000 --- a/src/ifcopenshell-python/test/ids_doc_generator.py +++ /dev/null @@ -1,125 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Thomas Krijnen -# -# This file is part of IfcOpenShell. -# -# IfcOpenShell 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, -# 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 . - -import os -import re -import unittest -import functools -import itertools -import ifcopenshell -import test_ids -from xml.dom.minidom import parseString -from ifcopenshell import ids, template, validate - -outdir = "build" - - -class DocGenerator: - def __init__(self): - self.facet = None - self.testcases = {} - - def __call__(self, name, *, facet, inst, expected): - if not name: - return - - result = "pass" if expected is True else "fail" - - f = inst.wrapped_data.file - instances = list(f) - - # 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: - if "GlobalId" in issue["message"]: - issue["instance"].GlobalId = ifcopenshell.guid.new() - - elif "PredefinedType" in issue["message"]: - ty = re.findall("\\(.+?\\)", issue["message"])[0][1:-1].split(", ")[0] - issue["instance"].PredefinedType = ty - elif "IfcMaterialList" in issue["message"]: - issue["instance"].Materials = [f.createIfcMaterial("Concrete", None, "CONCRETE")] - else: - raise Exception("About to emit invalid example data:", issue) - - ifc_text = "\n".join([f"{e} /* Testcase */" if e == inst else str(e) for e in f]) - basename = f"{result}-" + re.sub("[^0-9a-zA-Z]", "_", name.lower()) - - # Write IFC to disk - f.write(os.path.join(outdir, 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.specifications.append(spec) - - # Write IDS to disk - with open(os.path.join(outdir, f"{basename}.ids"), "w", encoding="utf-8") as ids_file: - ids_file.write(specs.to_string()) - - xml_text = "\n".join( - l - for l in parseString(specs.to_string()) - .getElementsByTagName("requirements")[0] - .childNodes[1] - .toprettyxml() - .split("\n") - if l.strip() - ).replace("\t", " ") - - self.testcases.setdefault(self.facet, []).append( - {"name": name, "ids": xml_text, "ifc": ifc_text, "basename": basename, "result": result, "id": inst.id()} - ) - - assert bool(facet(inst)) is expected - - def set_facet(self, facet): - self.facet = facet - - -test_ids.run = DocGenerator() -test_ids.set_facet = test_ids.run.set_facet - -suite = unittest.TestLoader().discover(".", pattern="test_ids.py") -result = unittest.TextTestRunner(verbosity=2).run(suite) - -for facet, testcases in test_ids.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") - write() - write("These testcases are designed to help describe behaviour in edge cases and ambiguities. All valid IDS implementations must demonstrate identical behaviour to these test cases.") - write() - for testcase in testcases: - write(f"## [{testcase['result'].upper()}] {testcase['name']}") - write() - write("~~~xml") - write(testcase["ids"]) - write("~~~") - write() - write("~~~lua") - write(testcase["ifc"]) - write("~~~") - write() - write(f"[Sample IDS]({testcase['basename']}.ids) - [Sample IFC: {testcase['id']}]({testcase['basename']}.ifc)") - write() diff --git a/src/ifcopenshell-python/test/test_ids.py b/src/ifcopenshell-python/test/test_ids.py deleted file mode 100644 index 55ac1aa55e..0000000000 --- a/src/ifcopenshell-python/test/test_ids.py +++ /dev/null @@ -1,1381 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Thomas Krijnen -# -# This file is part of IfcOpenShell. -# -# IfcOpenShell 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, -# 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 . - -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")) - - -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.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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements.terms[0].node["propertySet"]["simpleValue"], "Test_PropertySet" - ) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual(ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual( - ids_file.specifications[0].requirements.terms[0].node["value"]["simpleValue"], "Test_Classification" - ) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["system"]["simpleValue"], "Test_System") - - def test_failing_on_opening_invalid_ids_data(self): - with pytest.raises(xmlschema.validators.exceptions.XMLSchemaValidationError): - ids.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.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.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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - [ - x["@value"] - for x in ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements.terms[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() == { - "@xmlns": "http://standards.buildingsmart.org/IDS", - "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", - "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", - "info": {"title": "Untitled"}, - "specifications": {"specification": []}, - } - - def test_create_an_ids_with_all_possible_information(self): - specs = ids.ids( - title="title", - copyright="copyright", - version="version", - description="description", - author="author@test.com", - date="2020-01-01", - purpose="purpose", - milestone="milestone", - ) - assert specs.asdict() == { - "@xmlns": "http://standards.buildingsmart.org/IDS", - "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", - "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", - "info": { - "title": "title", - "copyright": "copyright", - "version": "version", - "description": "description", - "author": "author@test.com", - "date": "2020-01-01", - "purpose": "purpose", - "milestone": "milestone", - }, - "specifications": {"specification": []}, - } - - def test_check_invalid_ids_information(self): - specs = ids.ids(title=None, author="author", date="9999-99-99") - assert specs.asdict() == { - "@xmlns": "http://standards.buildingsmart.org/IDS", - "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", - "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd", - "info": {"title": "Untitled"}, - "specifications": {"specification": []}, - } - - def test_authoring_an_ids_with_no_specifications_is_invalid(self): - specs = ids.ids() - 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.create(name="IfcName") - assert facet.asdict() == {"name": {"simpleValue": "IfcName"}} - facet = ids.entity.create(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.create(name="IFCRABBIT") - run("Invalid entities always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) - - ifc = ifcopenshell.file() - facet = ids.entity.create(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.create(name="IfcWall") - ifc = ifcopenshell.file() - run( - "Entities must be specified as uppercase strings", - facet=facet, - inst=ifc.createIfcWall(), - expected=False, - ) - - facet = ids.entity.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(name="IFCWALL", predefinedType="X") - run("Overridden predefined types should pass", facet=facet, inst=wall, expected=True) - - restriction = ids.restriction.create(options=["IFCWALL", "IFCSLAB"], type="enumeration") - facet = ids.entity.create(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.create(options="IFC.*TYPE", type="pattern") - facet = ids.entity.create(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.create(options="FOO.*", type="pattern") - facet = ids.entity.create(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.create(name="name") - assert attribute.asdict() == {"name": {"simpleValue": "name"}} - attribute = ids.attribute.create(name="name", value="value") - assert attribute.asdict() == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}} - attribute = ids.attribute.create( - 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.create(name="Foobar") - run("Invalid attribute names always fail", facet=facet, inst=ifc.createIfcWall(), expected=False) - - ifc = ifcopenshell.file() - facet = ids.attribute.create(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.create(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.create(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.create(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.create(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.create(name="LayerOn") - run("Attributes with a logical unknown always fail", facet=facet, inst=layer, expected=False) - - facet = ids.attribute.create(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.create(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.create(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.create(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.create(name="Dim") - run("Derived attributes cannot be checked and always fail", facet=facet, inst=ifc.createIfcCartesianPoint([0., 0., 0.]), expected=False) - - facet = ids.attribute.create(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) - - facet = ids.attribute.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(name="IsMilestone", value="FALSE") - run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True) - facet = ids.attribute.create(name="IsMilestone", value="False") - run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=False) - - facet = ids.attribute.create(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.create(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.create(options=".*Name.*", type="pattern") - facet = ids.attribute.create(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.create(options=["Name", "Description"], type="enumeration") - facet = ids.attribute.create(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.create(options=["Foo", "Bar"], type="enumeration") - facet = ids.attribute.create(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.create(options=["42"], type="enumeration", base="string") - facet = ids.attribute.create(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.create( - options={"minInclusive": 42, "maxInclusive": 42}, type="bounds", base="decimal" - ) - facet = ids.attribute.create(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.create() - assert facet.asdict() == {} - facet = ids.classification.create(value="value", system="system") - assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}} - facet = ids.classification.create( - 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.create() - 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.create(value="1") - run("Values should match exactly if lightweight classifications are used", facet=facet, inst=element1, expected=True) - - facet = ids.classification.create(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.create(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.create(options="1.*", type="pattern") - facet = ids.classification.create(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.create(options="Foo.*", type="pattern") - facet = ids.classification.create(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.create(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.create(value="11") - run("Occurrences override the type classification per system 1/3", facet=facet, inst=wall, expected=True) - facet = ids.classification.create(value="22") - run("Occurrences override the type classification per system 2/3", facet=facet, inst=wall, expected=False) - facet = ids.classification.create(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.create() - assert facet.asdict() == { - "propertySet": {"simpleValue": "Property_Set"}, - "name": {"simpleValue": "PropertyName"}, - } - facet = ids.property.create( - 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): - 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.create(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.create(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.create(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.create(options="Foo_.*", type="pattern") - facet = ids.property.create(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.create(options="Foo.*", type="pattern") - facet = ids.property.create(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.create(options="Foo.*", type="pattern") - restriction2 = ids.restriction.create(options=["x", "y"], type="enumeration") - facet = ids.property.create(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.create(options=[42.12], type="enumeration", base="decimal") - facet = ids.property.create(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.create(options=[42], type="enumeration", base="integer") - facet = ids.property.create(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.create(options=[True], type="enumeration", base="boolean") - facet = ids.property.create(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.create(options=[42.12], type="enumeration", base="decimal") - facet = ids.property.create(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.create(options=[2], type="enumeration", base="decimal") - facet = ids.property.create(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.create(options=[2], type="enumeration", base="decimal") - facet = ids.property.create(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.create(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.create(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.create() - assert facet.asdict() == {} - facet = ids.material.create( - 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): - ifc = ifcopenshell.file() - - # A material facet with no data matches any present material - facet = ids.material.create() - element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - run("", 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("", facet=facet, inst=element, expected=True) - - # A value will match a material name or category - facet = ids.material.create(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("", facet=facet, inst=element, expected=False) - material.Name = "Foo" - run("", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - - # A value will match any material name or category in a material list - facet = ids.material.create(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("", 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("", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - - # A value will match any material name or category, or layer name or category in a layer set - facet = ids.material.create(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) - run("", facet=facet, inst=element, expected=False) - 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("", facet=facet, inst=element, expected=True) - layer.Name = "Bar" - layer.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - layer.Category = "Bar" - material.Name = "Foo" - run("", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - - # A value will match any material name or category, or profile name or category in a profile set - facet = ids.material.create(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) - run("", facet=facet, inst=element, expected=False) - 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" - run("", facet=facet, inst=element, expected=True) - profile.Name = "Bar" - profile.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - profile.Category = "Bar" - material.Name = "Foo" - run("", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - - # A value will match any material name or category, or constituent name or category in a constituent set - facet = ids.material.create(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("", 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("", facet=facet, inst=element, expected=True) - constituent.Name = "Bar" - constituent.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - constituent.Category = "Bar" - material.Name = "Foo" - run("", facet=facet, inst=element, expected=True) - material.Name = "Bar" - material.Category = "Foo" - run("", facet=facet, inst=element, expected=True) - - # The facet will check for inherited materials - 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.create(value="Foo") - run("", facet=facet, inst=element, expected=True) - run("", facet=facet, inst=element_type, expected=True) - - # The facet will check for overriden materials - 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.create(value="Foo") - run("", facet=facet, inst=element, expected=True) - run("", facet=facet, inst=element_type, expected=False) - - def test_creating_a_partof_facet(self): - facet = ids.partOf.create() - assert facet.asdict() == {"@entity": "IfcSystem"} - facet = ids.partOf.create(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.create(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.create(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.create(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.create(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.create(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.create(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.create(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].add_applicability(ids.entity.create(name="Test_Name")) - r = ids.restriction.create(options=["testA", "testB"], type="enumeration") - m = ids.material.create(value=r) - i.specifications[0].add_requirement(m) - self.assertEqual(i.specifications[0].requirements.terms[0].value, "testA") - self.assertEqual(i.specifications[0].requirements.terms[0].value, "testB") - self.assertNotEqual(i.specifications[0].requirements.terms[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].add_applicability(ids.entity.create(name="Test_Name")) - r = ids.restriction.create(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") - p = ids.property.create(propertySet="Test", name="Test", value=r) - i.specifications[0].add_requirement(p) - self.assertEqual(i.specifications[0].requirements.terms[0].value, 0) - self.assertEqual(i.specifications[0].requirements.terms[0].value, 5) - self.assertNotEqual(i.specifications[0].requirements.terms[0].value, -1) - self.assertNotEqual(i.specifications[0].requirements.terms[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].add_applicability(ids.entity.create(name="Test_Name")) - r = ids.restriction.create(options="[A-Z]{2,4}", type="pattern") - p = ids.property.create(propertySet="Test", name="Test", value=r) - i.specifications[0].add_requirement(p) - self.assertEqual(i.specifications[0].requirements.terms[0].value, "XYZ") - self.assertNotEqual(i.specifications[0].requirements.terms[0].value, "abc") - self.assertNotEqual(i.specifications[0].requirements.terms[0].value, "ABCDE") - self.assertNotEqual(i.specifications[0].requirements.terms[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].add_applicability(ids.entity.create(name="Test_Name")) - r = ids.restriction.create(options="(Wanddurchbruch|Deckendurchbruch).*", type="pattern") - p = ids.property.create(propertySet="Test", name="Test", value=r) - i.specifications[0].add_requirement(p) - self.assertEqual(i.specifications[0].requirements.terms[0].value, "Wanddurchbruch") - self.assertEqual(i.specifications[0].requirements.terms[0].value, "Deckendurchbruch") - self.assertNotEqual(i.specifications[0].requirements.terms[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].add_applicability(ids.entity.create(name="Test_Name")) - r = ids.restriction.create(options="èêóòâôæøåążźćęóʑʒʓʔʕʗʘʙʚʛʜʝʞ", type="pattern") - p = ids.property.create(propertySet="Test", name="Test", value=r) - i.specifications[0].add_requirement(p) - self.assertEqual(i.specifications[0].requirements.terms[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.create(name="Test_Name", predefinedType="Test_PredefinedType") - c = ids.classification.create(value="Test_Value", system="Test_System") - m = ids.material.create(value="Test_Value") - re = ids.restriction.create(options=["testA", "testB"], type="enumeration") - rb = ids.restriction.create(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") - rp1 = ids.restriction.create(options="[A-Z]{2,4}", type="pattern") - rp2 = ids.restriction.create(options="èêóòâôæøåążźćęóʑʒʓʔʕʗʘʙʚʛʜʝʞ", type="pattern") - p1 = ids.property.create(propertySet="Test_PropertySet", name="Test_Parameter", value=re) - p2 = ids.property.create(propertySet="Test_PropertySet", name="Test_Parameter", value=rb) - p3 = ids.property.create(propertySet="Test_PropertySet", name="Test_Parameter", value=rp1) - p4 = ids.property.create(propertySet="Test_PropertySet", name="Test_Parameter", value=rp2) - p5 = ids.property.create(propertySet="Test_PropertySet", name="Test_Parameter", value=[re, rb, rp1]) - i.specifications[0].add_applicability(e) - i.specifications[0].add_applicability(m) - i.specifications[0].add_requirement(c) - i.specifications[0].add_requirement(p1) - i.specifications[0].add_requirement(p2) - i.specifications[0].add_requirement(p3) - i.specifications[0].add_requirement(p4) - i.specifications[0].add_requirement(p5) - fn = "TEST_FILE.xml" - result = i.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") - spec.add_applicability(ids.entity.create(name="IFCWALL")) - spec.add_requirement(ids.attribute.create(name="Name", value="Waldo")) - specs.specifications.append(spec) - assert "http://standards.buildingsmart.org/IDS" in specs.to_string() - assert spec.status == None - - model = ifcopenshell.file() - wall = model.createIfcWall() - waldo = model.createIfcWall(Name="Waldo") - specs.validate2(model) - - assert spec.status == False - assert set(spec.applicable_entities) == {wall, waldo} - assert spec.failed_entities == [wall] - - def test_creating_multiple_specifications(self): - specs = ids.ids(title="Title") - spec = ids.specification(name="Name") - spec.add_applicability(ids.entity.create(name="IFCWALL")) - spec.add_requirement(ids.attribute.create(name="Name", value="Waldo")) - specs.specifications.append(spec) - - spec2 = ids.specification(name="Name") - spec2.add_applicability(ids.entity.create(name="IFCWALL")) - spec2.add_requirement(ids.attribute.create(name="Name", value="Waldo")) - specs.specifications.append(spec2) - - model = ifcopenshell.file() - wall = model.createIfcWall() - waldo = model.createIfcWall(Name="Waldo") - specs.validate2(model) - - assert spec.status == False - assert set(spec.applicable_entities) == {wall, waldo} - assert spec.failed_entities == [wall] - assert spec2.failed_entities == [wall] - - def test_validate_simple(self): - return # TODO - # Same test as in reporting... - ids_file = ids.ids.open(IDS_URL) - report = ids.SimpleHandler() - logger.addHandler(report) - ids_file.validate(ifc_file, logger) - self.assertEqual(len(report.statements), 5) - logger.handlers.pop() - - def test_validate_all_facets(self): - # Those are true: - e = ids.entity.create(name="IfcWall") - p1 = ids.property.create(propertySet="MySet", name="Param1", value="banan") - p2 = ids.property.create(propertySet="MySet", name="Param2", value=120.0) - p3 = ids.property.create(propertySet="Pset_WallCommon", name="LoadBearing", value=False) - # Those are false: - p4 = ids.property.create(propertySet="MySet", name="Param1", value="orange") - p5 = ids.property.create(propertySet="MySet", name="Param2", value=123.4) - p6 = ids.property.create(propertySet="Pset_WallCommon", name="LoadBearing", value=True) - - i = ids.ids(title="My IDS") - i.specifications.append(ids.specification(name="Test_Specification")) - i.specifications[0].add_applicability(e) - i.specifications[0].add_requirement(p1) - i.specifications[0].add_requirement(p2) - i.specifications[0].add_requirement(p3) - i.specifications[0].add_requirement(p4) - i.specifications[0].add_requirement(p5) - i.specifications[0].add_requirement(p6) - - report = ids.SimpleHandler(report_valid=True) - logger.addHandler(report) - - i.validate(ifc_file, logger) - # 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", - ) - ids_file = ids.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - [ - x["@value"] - for x in ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - self.assertEqual(ids_file.specifications[0].requirements.terms[0].node["name"]["simpleValue"], "Test_Parameter") - self.assertEqual( - ids_file.specifications[0].requirements.terms[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.ids.open(IDS_URL) - report = ids.SimpleHandler() - logger.addHandler(report) - ids_file.validate(ifc_file, logger) - self.assertEqual(len(report.statements), 5) - logger.handlers.pop() - - def test_bcf_report(self): - return # TODO - ids_file = ids.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, logger) - my_bcfxml = bcfxml.load(fn) - topics = my_bcfxml.get_topics() - self.assertEqual(len(topics), 5) - logger.handlers.pop()