import operator import ifcopenshell.util.element import re from xmlschema import XMLSchema from xmlschema import etree_tostring from xmlschema.validators import facets from xmlschema.validators import identities ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd") class exception(Exception): pass def error(msg): raise exception(msg) 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. """ def __init__(self, node): self.node = node def __getattr__(self, k): if k in self.node: v = self.node[k] if isinstance(v, dict): #is restriction? return restriction(v['xs:restriction'][0]) else: return v 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"] def __call__(self, inst, logger): # @nb with inheritance if self.predefinedtype and hasattr(inst, "PredefinedType"): self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'" return facet_evaluation( inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype, self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType} ) else: self.message = "an entity name '%(name)s'" return facet_evaluation( inst.is_a(self.name), self.message % {"name": inst.is_a()} ) class classification(facet): """ The IDS classification facet by traversing the HasAssociations inverse attribute """ parameters = ["system", "value", "location"] message = "%(location)sclassification reference %(value)s from '%(system)s'" def __call__(self, inst, logger): self.location = self.node['@location'] instance_classiciations = inst.HasAssociations if ifcopenshell.util.element.get_type(inst): type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations else: type_classifications = () if self.location == 'instance' and instance_classiciations: associations = instance_classiciations elif self.location == 'type' and type_classifications: associations = type_classifications elif self.location == 'any' and (instance_classiciations or type_classifications): associations = instance_classiciations + type_classifications else: associations = () refs = [] for association in associations: if association.is_a("IfcRelAssociatesClassification"): cref = association.RelatingClassification if hasattr(cref, 'ItemReference'): #IFC2x3 refs.append((cref.ReferencedSource.Name, cref.ItemReference)) elif hasattr(cref, 'Identification'): # IFC4 refs.append((cref.ReferencedSource.Name, cref.Identification)) self.location = location[self.location] if refs: return facet_evaluation( (self.system, self.value) in refs, self.message % {"system": refs[0][0], "value": "'"+refs[0][1]+"'", "location": self.location} # what if not first item of refs? ) else: return facet_evaluation( False, "does not have %sclassification reference" % self.location ) class property(facet): """ The IDS property facet implemented using `ifcopenshell.util.element` """ parameters = ["name", "propertyset", "value", "location"] message = "%(location)sproperty '%(name)s' in '%(propertyset)s' with a value %(value)s" def __call__(self, inst, logger): self.location = self.node['@location'] instance_props = ifcopenshell.util.element.get_psets(inst) if ifcopenshell.util.element.get_type(inst): type_props = ifcopenshell.util.element.get_psets( ifcopenshell.util.element.get_type(inst) ) else: type_props = {} if self.location == 'instance': props = instance_props elif self.location == 'type' and type_props: props = type_props elif self.location == 'any' and (instance_props or type_props): props = {**instance_props , **type_props} else: props = {} pset = props.get(self.propertyset) val = pset.get(self.name) if pset else None self.location = location[self.location] di = { "name": self.name, "propertyset": self.propertyset, "value": "'%s'" % val, "location": self.location } if val is not None: msg = self.message % di else: if pset: msg = "does not have %(location)sproperty '%(name)s' in a set '%(propertyset)s'" % di else: msg = "does not have %(location)sset '%(propertyset)s'" % di #TODO implement data type comparison return facet_evaluation( val == self.value, msg ) class material(facet): """ The IDS material facet by traversing the HasAssociations inverse attribute """ parameters = ["value", "location"] message = "%(location)smaterial '%(value)s'" def __call__(self, inst, logger): self.location = self.node['@location'] instance_material_rel = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] if ifcopenshell.util.element.get_type(inst): type_material_rel = [rel for rel in ifcopenshell.util.element.get_type(inst).HasAssociations if rel.is_a("IfcRelAssociatesMaterial")] else: type_material_rel = [] if self.location == 'instance': material_relations = list(instance_material_rel) elif self.location == 'type' and type_material_rel: material_relations = list(type_material_rel) elif self.location == 'any' and (instance_material_rel or type_material_rel): material_relations = instance_material_rel + type_material_rel else: material_relations = [] materials = [] for rel in material_relations: if rel.RelatingMaterial.is_a() == "IfcMaterial": materials.append(rel.RelatingMaterial.Name) elif rel.RelatingMaterial.is_a() == "IfcMaterialMaterialList": #DEPRECATED in IFC4 [materials.append(mat.Name) for mat in rel.RelatingMaterial] elif rel.RelatingMaterial.is_a() == "IfcMaterialConstituentSet": [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialConstituents] elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSet": [materials.append(mat.Name) for mat in rel.RelatingMaterial.MaterialLayers] elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage": layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers [materials.append(layer.Material.Name) for layer in layers] elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSet": [materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialProfiles] elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSetUsage": profileSets = rel.RelatingMaterial.ForProfileSet.MaterialProfiles [materials.append(pset.Material.Name) for pset in profileSets] else: logger.error({'guid':inst.GlobalId, 'result':'ERROR', 'sentence':'IfcRelAssociatesMaterial not implemented'}) if not materials: materials.append('UNDEFINED') self.location = location[self.location] return facet_evaluation( self.value in materials, self.message % {"value": "'/'".join(materials), "location": self.location}, ) 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 class restriction: """ The value restriction from XSD implemented as a list of values and a containment test """ def __init__(self, node): self.restriction_on = node['@base'][3:] self.type = "" self.options = [] for n in node: if n[0:3] == "xs:": if n[3:] == "enumeration": self.type = "enumeration" for x in node[n]: self.options.append(x["@value"]) elif n[8:] == "clusive": self.type = "bounds" if n[3:6] == 'min': self.options.insert(0,'>') else: self.options.insert(0,'<') if n[6:9] == 'Inc': self.options[0] += '=' self.options[0] += node[n]['@value'] elif n[-5:] == "ength": self.type = "length" if n[3:6] == "min": self.options.append('>=') elif n[3:6] == "max": self.options.append('<=') else: self.options.append('==') self.options[-1] += str(node[n]['@value']) elif n[3:] == "pattern": self.type = "pattern" self.options.append(node[n]['@value']) #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace else: logger.error({'result':'ERROR', 'sentence':'Restriction not implemented'}) def __eq__(self, other): result=False #TODO implement data type comparison if self and other: if self.type == "enumeration" and self.restriction_on == 'bool': self.options = [x.lower() for x in self.options] result = str(other).lower() in self.options elif self.type == "enumeration": result = other in self.options elif self.type == "bounds": for op in self.options: if eval(str(other)+op): #TODO eval not safe? result = True 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": self.options translated_pattern = identities.translate_pattern(r'[A-Z]{1,3}') # Between one and three capital letters 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): if self.type == "enumeration": return "'%s'" % "' or '".join(self.options) elif self.type == "bounds": self.options.sort() return "of type '%s', having a value %s" % (self.restriction_on, ' and '.join(self.options)) elif self.type == "length": return "of type '%s' with %s letters" % (self.restriction_on, ' and '.join(self.options)) elif self.type == "pattern": return "of type '%s' respecting pattern '%s'" % (self.restriction_on, ' and '.join(self.options)) #TODO add fractionDigits #TODO add totalDigits #TODO add whiteSpace class specification: """ Represents the XML node and its two children and """ def __init__(self, node): def parse_rules(node): names = [req for req in node for n in node[req]] children = [child for req in node for child in node[req]] classes = map(meta_facet.facets.__getitem__, names) return [cls(n) for cls, n in zip(classes, children)] self.applicability = boolean_and(parse_rules(node['applicability'])) self.requirements = boolean_and(parse_rules(node['requirements'])) def __call__(self, inst, logger): 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"}) return True, True else: 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"}) return True, False else: return False, False def __str__(self): return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__ class ids: """ Represents the XML root node and its childNodes. """ @staticmethod def parse(fn, ids_schema=ids_schema): ids_schema.validate(fn) ids_content = ids_schema.to_dict(fn) new_ids = ids() new_ids.specifications = [specification(s) for s in ids_content['specification']] return new_ids def validate(self, ifc_file, logger): self.ifc_checked = 0 self.ifc_passed = 0 for spec in self.specifications: for elem in ifc_file.by_type("IfcObject"): apply, comply = spec(elem, logger) if apply: self.ifc_checked += 1 if comply: self.ifc_passed += 1 location = { 'instance': 'an instance ', 'type': 'a type ', 'any': 'a ' } if __name__ == "__main__": import time start_time = time.time() import sys, os import logging import ifcopenshell from datetime import date filename = os.path.join(os.getcwd(), str(date.today())+"_ids_result.txt") logger = logging.getLogger("IDS") logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s") logging.FileHandler(filename, mode='w') ifc_file = ifcopenshell.open(sys.argv[2]) ids_file = ids.parse(sys.argv[1]) ids_file.validate(ifc_file, logger) print("Out of %s IFC elements, %s were checked against %s requirements in %s specification(s) and %s of them passed (%s).\nRuntime=%ss. Results saved to %s" % (len(ifc_file.by_type('IfcProduct')), ids_file.ifc_checked, len(ids_file.specifications[0].requirements.terms), len(ids_file.specifications), ids_file.ifc_passed, str(ids_file.ifc_passed/ids_file.ifc_checked*100)+'%', round(time.time() - start_time, 2), filename))