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 ) :
2021-04-09 10:16:08 +02:00
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
2021-04-30 08:41:33 +02:00
if self . predefinedtype and hasattr ( inst , " PredefinedType " ) :
2021-04-29 20:46:55 +02:00
# logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.predefinedtype)
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 " ]
2021-04-09 10:16:08 +02:00
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-04-09 10:16:08 +02:00
refs . append ( ( cref . ReferencedSource . Name , cref . ItemReference ) )
2021-02-27 11:04:45 +01:00
2021-02-28 09:31:21 +01:00
return facet_evaluation (
( self . system , self . value ) in refs ,
# @todo
2021-04-09 10:16:08 +02:00
" [classification_eval_todo] " ,
2021-02-28 09:31:21 +01:00
)
2021-02-27 11:04:45 +01:00
class property ( facet ) :
"""
The IDS property facet implenented using `ifcopenshell.util.element`
"""
2021-04-09 10:16:08 +02:00
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 )
2021-04-09 10:16:08 +02:00
val = pset . get ( self . name ) if pset else None
2021-04-29 20:46:55 +02:00
logger . debug ( " Testing if property %s == %s " , val , self . value )
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
di = {
2021-04-09 10:16:08 +02:00
" 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-04-28 00:10:13 +02:00
msg = " a set ' %(propertyset)s ' , but no property ' %(name)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
return facet_evaluation ( val == self . value , msg )
2021-02-27 11:04:45 +01:00
2021-04-09 10:16:08 +02:00
class material ( facet ) :
"""
The IDS material facet
"""
parameters = [ " name " , " value " ]
message = " a material ' %(name)s with value ' %(value)s ' "
def __call__ ( self , inst , logger ) :
material_relations = [ rel for rel in inst . HasAssociations if rel . is_a ( " IfcRelAssociatesMaterial " ) ]
names = [ ]
for rel in material_relations :
2021-04-28 00:10:13 +02:00
# @todo not all subtypes of IfcMaterial handled
2021-04-09 10:16:08 +02:00
if rel . RelatingMaterial . is_a ( ) == " IfcMaterialLayerSetUsage " :
layers = rel . RelatingMaterial . ForLayerSet . MaterialLayers
names = [ layer . Material . Name for layer in layers ]
elif rel . RelatingMaterial . is_a ( ) == " IfcMaterial " :
names . append ( rel . RelatingMaterial . Name )
return facet_evaluation (
0 ,
# @todo
" [material_eval_todo] " ,
)
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-02-28 09:31:21 +01:00
eval = [ t ( * args ) for t in self . terms ]
join = [ " and " , " or " ] [ self . fold == any ]
2021-02-28 09:34:35 +01:00
return facet_evaluation ( self . fold ( eval ) , join . join ( map ( str , eval ) ) )
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 ) :
2021-04-09 10:16:08 +02:00
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 ) :
2021-04-09 10:16:08 +02:00
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-04-09 10:16:08 +02:00
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 \n We 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 ( )
2021-04-09 10:16:08 +02:00
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-04-09 10:16:08 +02:00
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 " )
2021-04-09 10:16:08 +02:00
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 ). \n Runtime= %s s. 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 ) )