Files
IfcOpenShell/src/ifcopenshell-python/ifcopenshell/ids.py
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

355 lines
12 KiB
Python
Raw Normal View History

2021-02-27 11:04:45 +01:00
import operator
import ifcopenshell.util.element
2021-06-08 10:34:02 +02:00
from xmlschema import XMLSchema
2021-02-27 11:04:45 +01:00
2021-02-28 09:34:35 +01:00
class exception(Exception):
pass
2021-02-27 11:04:45 +01:00
def error(msg):
raise exception(msg)
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
class facet_evaluation:
"""
The evaluation of a facet with data from IFC. Converts to bool and has a human readable string format.
"""
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __init__(self, success, str):
self.success = success
self.str = str
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __bool__(self):
return self.success
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
return self.str
2021-02-27 11:04:45 +01:00
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.
2021-06-10 15:19:23 +02:00
"""
2021-02-27 11:04:45 +01:00
def __init__(self, node):
self.node = node
def __getattr__(self, k):
2021-06-10 15:19:23 +02:00
if k in self.node:
v = self.node[k]
else:
2021-04-30 11:06:36 +02:00
v = None
2021-06-10 15:19:23 +02:00
return v
#TODO implement restrictons
"""
2021-04-30 11:06:36 +02:00
if v:
2021-04-29 20:46:55 +02:00
elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE]
if elems:
return restriction(elems[0])
else:
return v.firstChild.nodeValue.strip()
2021-04-30 11:06:36 +02:00
else:
2021-04-29 20:46:55 +02:00
return None
2021-06-10 15:19:23 +02:00
"""
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __iter__(self):
for k in self.parameters:
yield k, getattr(self, k)
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
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
2021-02-27 11:04:45 +01:00
class entity(facet):
"""
The IDS entity facet currently *with* inheritance
"""
2021-02-28 09:34:35 +01:00
2021-04-29 20:46:55 +02:00
parameters = ["name", "predefinedtype"]
2021-02-27 11:04:45 +01:00
def __call__(self, inst, logger):
# @nb with inheritance
if self.predefinedtype and hasattr(inst, "PredefinedType"):
2021-04-29 20:46:55 +02:00
self.message = "an entity name '%(name)s' of predefined type '%(predefinedtype)s'"
2021-06-10 15:19:23 +02:00
return facet_evaluation(
inst.is_a(self.name) and inst.PredefinedType == self.predefinedtype,
self.message % {"name": inst.is_a(), "predefinedtype": inst.PredefinedType}
)
2021-04-29 20:46:55 +02:00
else:
self.message = "an entity name '%(name)s'"
2021-06-10 15:19:23 +02:00
return facet_evaluation(
inst.is_a(self.name),
self.message % {"name": inst.is_a()}
)
2021-04-30 00:09:04 +02:00
2021-02-27 11:04:45 +01:00
class classification(facet):
"""
The IDS classification facet by traversing the HasAssociations inverse attribute
"""
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
parameters = ["system", "value"]
message = "a classification reference '%(value)s' from '%(system)s'"
2021-02-27 11:04:45 +01:00
def __call__(self, inst, logger):
refs = []
for association in inst.HasAssociations:
if association.is_a("IfcRelAssociatesClassification"):
cref = association.RelatingClassification
2021-06-10 15:21:51 +02:00
refs.append((cref.ReferencedSource.Name, cref.Identification)) # before was .ItemReference instead of .Identification
2021-02-27 11:04:45 +01:00
2021-06-10 15:21:51 +02:00
if refs:
return facet_evaluation(
(self.system, self.value) in refs,
self.message % {"system": refs[0][0], "value": refs[0][1]}
)
else:
return facet_evaluation(
2021-06-11 17:10:54 +02:00
False,
2021-06-10 15:21:51 +02:00
"has no classification"
)
2021-02-27 11:04:45 +01:00
2021-06-11 17:10:54 +02:00
2021-02-27 11:04:45 +01:00
class property(facet):
"""
The IDS property facet implenented using `ifcopenshell.util.element`
"""
parameters = ["name", "propertyset", "value"]
message = "a property '%(name)s' in '%(propertyset)s' with value '%(value)s'"
2021-02-28 09:31:21 +01:00
2021-02-27 11:04:45 +01:00
def __call__(self, inst, logger):
props = ifcopenshell.util.element.get_psets(inst)
2021-02-28 09:31:21 +01:00
pset = props.get(self.propertyset)
val = pset.get(self.name) if pset else None
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
di = {
"name": self.name,
2021-02-28 09:34:35 +01:00
"propertyset": self.propertyset,
"value": val,
2021-02-28 09:31:21 +01:00
}
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
if val is not None:
msg = self.message % di
else:
if pset:
2021-06-11 17:10:54 +02:00
msg = "no property '%(name)s' in a set '%(propertyset)s'" % di
2021-02-28 09:31:21 +01:00
else:
msg = "no set '%(propertyset)s'" % di
2021-02-28 09:34:35 +01:00
2021-06-11 17:10:54 +02:00
return facet_evaluation(
str(val) == str(self.value),
msg
)
2021-02-27 11:04:45 +01:00
class material(facet):
"""
2021-06-11 17:10:54 +02:00
The IDS material facet by traversing the HasAssociations inverse attribute
"""
2021-06-11 17:10:54 +02:00
parameters = ["value"]
message = "a material '%(value)s'"
def __call__(self, inst, logger):
material_relations = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")]
2021-06-11 17:10:54 +02:00
materials = []
for rel in material_relations:
2021-06-11 17:10:54 +02:00
#TODO test all subtypes of material definitions
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
2021-06-11 17:10:54 +02:00
[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'})
return facet_evaluation(
2021-06-11 17:10:54 +02:00
self.value in materials,
self.message % {"value": self.value},
)
2021-02-27 11:04:45 +01:00
class boolean_logic:
"""
Boolean conjunction over a collection of functions
"""
def __init__(self, terms):
self.terms = terms
def __call__(self, *args):
2021-06-11 17:10:54 +02:00
#TODO only takes one requirement of a type, should take all
2021-02-28 09:31:21 +01:00
eval = [t(*args) for t in self.terms]
join = [" and ", " or "][self.fold == any]
2021-06-11 17:10:54 +02:00
return facet_evaluation(
self.fold(eval),
join.join(map(str, eval))
)
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
return [" and ", " or "][self.fold == any].join(map(str, self.terms))
2021-02-27 11:04:45 +01:00
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.getAttribute("base")
self.options = []
self.type = []
2021-06-10 15:19:23 +02:00
#TODO implement restrictions
# for n in node.childNodes:
# if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration"):
# self.options.append(n.getAttribute("value"))
# self.type = "enumeration"
# elif n.nodeType == n.ELEMENT_NODE and (n.tagName.endswith("Inclusive") or n.tagName.endswith("Exclusive")):
# self.options.append(n.getAttribute("value"))
# self.type = "bounds"
# elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("length"):
# self.options.append(n.getAttribute("value"))
# self.type = "length"
# elif n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("pattern"):
# self.options.append(n.getAttribute("value"))
# self.type = "pattern"
#TODO add min/maxLength
#TODO add fractionDigits
#TODO add totalDigits
#TODO add whiteSpace
2021-04-28 00:10:13 +02:00
2021-02-27 11:04:45 +01:00
def __eq__(self, other):
return other in self.options
def __repr__(self):
if self.type == "enumeration":
return " or ".join(self.options)
elif self.type == "bounds":
self.options.sort()
return "of type %s, having a value between %s and %s" % (self.restriction_on, self.options[0], self.options[1])
elif self.type == "length":
return "of type %s with a length of %s" % (self.restriction_on, self.options[0])
elif self.type == "pattern":
return "of type %s respecting pattern %s" % (self.restriction_on, self.options[0])
2021-04-29 20:46:55 +02:00
2021-02-27 11:04:45 +01:00
class specification:
"""
Represents the XML <specification> node and its two children <applicability> and <requirements>
"""
def __init__(self, node):
def parse_rules(node):
2021-06-10 15:19:23 +02:00
children = [node[n][0] for n in node]
names = [n for n in node]
2021-02-27 11:04:45 +01:00
classes = map(meta_facet.facets.__getitem__, names)
return [cls(n) for cls, n in zip(classes, children)]
2021-06-10 15:19:23 +02:00
self.applicability = boolean_and(parse_rules(node['applicability']))
self.requirements = boolean_and(parse_rules(node['requirements']))
2021-02-27 11:04:45 +01:00
def __call__(self, inst, logger):
2021-04-28 00:10:13 +02:00
if self.applicability(inst, logger):
2021-06-10 15:21:07 +02:00
global ifc_checked
ifc_checked += 1
2021-02-28 09:31:21 +01:00
valid = self.requirements(inst, logger)
2021-02-28 09:31:21 +01:00
if valid:
2021-06-10 15:21:07 +02:00
global ifc_passed
ifc_passed += 1
2021-06-10 15:19:23 +02:00
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"})
2021-02-27 11:04:45 +01:00
else:
2021-06-10 15:19:23 +02:00
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"})
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
2021-04-28 00:10:13 +02:00
return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__
2021-02-27 11:04:45 +01:00
class ids:
"""
Represents the XML root <ids> node and its <specification> childNodes.
"""
def __init__(self, fn):
2021-06-08 10:34:02 +02:00
ids_schema = XMLSchema("http://standards.buildingsmart.org/IDS/ids.xsd")
ids_schema.validate(fn)
2021-06-10 15:19:23 +02:00
ids = ids_schema.to_dict(fn)
self.specifications = [specification(s) for s in ids['specification']]
2021-02-27 11:04:45 +01:00
def validate(self, ifc_file, logger):
2021-02-28 09:31:21 +01:00
for spec in self.specifications:
for elem in ifc_file.by_type("IfcObject"):
2021-04-30 00:09:04 +02:00
spec(elem, logger)
2021-06-10 15:21:07 +02:00
ifc_checked = 0
ifc_passed = 0
2021-02-27 11:04:45 +01:00
if __name__ == "__main__":
2021-06-08 10:34:02 +02:00
import time
start_time = time.time()
import sys, os
2021-02-27 11:04:45 +01:00
import logging
import ifcopenshell
2021-06-08 10:34:02 +02:00
from datetime import date
2021-02-27 11:04:45 +01:00
2021-06-08 10:34:02 +02:00
filename = os.path.join(os.getcwd(), str(date.today())+"_ids_result.txt")
2021-02-27 11:04:45 +01:00
logger = logging.getLogger("IDS")
2021-04-29 20:46:55 +02:00
logging.basicConfig(filename=filename, level=logging.INFO, format="%(message)s")
logging.FileHandler(filename, mode='w')
2021-02-27 11:04:45 +01:00
2021-04-30 11:31:23 +02:00
ids_file = ids(sys.argv[1])
ifc_file = ifcopenshell.open(sys.argv[2])
2021-04-29 21:33:07 +02:00
2021-02-27 11:04:45 +01:00
ids_file.validate(ifc_file, logger)
2021-04-29 20:46:55 +02:00
2021-06-10 15:21:07 +02:00
print("Out of %s IFC elements, %s were checked against %s IDS requirements and %s of them passed (%s).\nRuntime=%ss. Results saved to %s"
% (len(ifc_file.by_type('IfcProduct')), ifc_checked, len(ids_file.specifications[0].requirements.terms), ifc_passed, str(ifc_passed/ifc_checked*100)+'%', round(time.time() - start_time, 2), filename))