2021-02-27 11:04:45 +01:00
import operator
import ifcopenshell . util . element
from xml . dom . minidom import parse
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.
"""
def __init__ ( self , node ) :
self . node = node
def __getattr__ ( self , k ) :
2021-05-01 09:09:35 +02:00
try :
v = self . node . getElementsByTagName ( k ) [ 0 ]
except IndexError :
v = None
if v :
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-02-27 11:04:45 +01:00
else :
2021-05-01 09:09:35 +02:00
return None
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-05-01 09:09:35 +02:00
parameters = [ " name " , " predefinedtype " ]
2021-04-23 12:13:41 +02:00
2021-02-27 11:04:45 +01:00
def __call__ ( self , inst , logger ) :
# @nb with inheritance
2021-05-01 09:09:35 +02:00
if self . predefinedtype and hasattr ( inst , " PredefinedType " ) :
# logger.debug("Testing if entity predefinedtype '%s' == '%s'", inst.PredefinedType, self.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 ( ) } )
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 " ]
# import pdb;pdb.set_trace()
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-05-01 09:09:35 +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-05-01 09:09:35 +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-23 12:14:52 +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 = [ ]
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 "
2021-05-01 09:09:35 +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-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 ) :
children = [ n for n in node . childNodes if n . nodeType == n . ELEMENT_NODE ]
names = map ( operator . attrgetter ( " tagName " ) , children )
classes = map ( meta_facet . facets . __getitem__ , names )
return [ cls ( n ) for cls , n in zip ( classes , children ) ]
phrases = [ n for n in node . childNodes if n . nodeType == n . ELEMENT_NODE ]
2021-02-28 09:34:35 +01:00
2021-02-27 11:04:45 +01:00
len ( phrases ) == 2 or error ( " expected two child nodes for <specification> " )
phrases [ 0 ] . tagName == " applicability " or error ( " expected <applicability> " )
phrases [ 1 ] . tagName == " requirements " or error ( " expected <requirements> " )
2021-02-28 09:34:35 +01:00
2021-05-01 09:09:35 +02:00
self . applicability , self . requirements = ( boolean_and ( parse_rules ( phrase ) ) for phrase in phrases )
2021-02-27 11:04:45 +01:00
def __call__ ( self , inst , logger ) :
2021-05-01 09:09:35 +02:00
if self . applicability ( inst , logger ) :
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-05-01 09:09:35 +02:00
logger . info ( { ' guid ' : inst . GlobalId , ' result ' : valid . success , ' sentence ' : str ( self ) + " \n ' " + inst . Name + " ' (id: " + inst . GlobalId + " ) has " + str ( valid ) + " so is compliant " } )
2021-02-27 11:04:45 +01:00
else :
2021-05-01 09:09:35 +02:00
logger . error ( { ' guid ' : inst . GlobalId , ' result ' : valid . success , ' sentence ' : str ( self ) + " \n ' " + inst . Name + " ' (id: " + inst . GlobalId + " ) 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-05-01 09:09:35 +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 ) :
dom = parse ( fn )
ids = dom . childNodes [ 0 ]
ids . tagName == " ids " or error ( " expected <ids> " )
self . specifications = [
2021-02-28 09:34:35 +01:00
specification ( n ) for n in ids . childNodes if n . nodeType == n . ELEMENT_NODE and n . tagName == " 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-02-27 11:04:45 +01:00
spec ( elem , logger )
2021-05-01 09:09:35 +02:00
2021-02-27 11:04:45 +01:00
if __name__ == " __main__ " :
2021-04-09 10:16:08 +02:00
import sys , os
2021-02-27 11:04:45 +01:00
import logging
import ifcopenshell
2021-04-09 10:16:08 +02:00
filename = os . path . join ( os . getcwd ( ) , " ids.txt " )
2021-02-27 11:04:45 +01:00
logger = logging . getLogger ( " IDS " )
2021-04-09 10:16:08 +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
ids_file = ids ( sys . argv [ 1 ] )
ifc_file = ifcopenshell . open ( sys . argv [ 2 ] )
2021-05-01 09:09:35 +02:00
2021-02-27 11:04:45 +01:00
ids_file . validate ( ifc_file , logger )
2021-05-01 09:09:35 +02:00
print ( " Validated %s IDS requirements on %s IFC elements. Results saved to %s " % ( len ( ids_file . specifications [ 0 ] . requirements . terms ) , len ( ifc_file . by_type ( ' IfcProduct ' ) ) , filename ) )