Coverage for bim2sim/tasks/common/check_ifc.py: 31%
431 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1from __future__ import annotations
3import types
4import inspect
5import json
6import logging
7import os
8import warnings
9from typing import Dict, Callable
11from ifcopenshell import file, entity_instance
12from mako.lookup import TemplateLookup
13from mako.template import Template
15from bim2sim.elements import hvac_elements as hvac, bps_elements as bps
16from bim2sim.elements.mapping import attribute
17from bim2sim.elements.mapping.ifc2python import get_property_sets, get_ports, \
18 get_layers_ifc
19from bim2sim.kernel.ifc_file import IfcFileClass
20from bim2sim.tasks.base import ITask, Playground
21from bim2sim.utilities.common_functions import all_subclasses
22from bim2sim.utilities.types import IFCDomain
25class CheckIfc(ITask):
26 """
27 Check an IFC file, for a number of conditions (missing information,
28 incorrect information, etc) that could lead on future tasks to fatal errors.
29 """
30 reads = ('ifc_files',)
32 def __init__(self, playground: Playground):
33 super().__init__(playground)
34 self.error_summary_sub_inst: dict = {}
35 self.error_summary_inst: dict = {}
36 self.error_summary_prop: dict = {}
37 self.sub_inst: list = []
38 self.id_list: list = []
39 self.elements: list = []
40 self.ps_summary: dict = {}
41 self.ifc_units: dict = {}
42 self.sub_inst_cls = None
43 self.plugin = None
45 def run(self, ifc_files: [IfcFileClass]) -> [dict, dict]:
46 """
47 Analyzes sub_elements and elements of an IFC file for the validation
48 functions and export the errors found as .json and .html files.
50 Args:
51 ifc_files: bim2sim IfcFileClass holding the ifcopenshell ifc
52 instance
54 Returns:
55 error_summary_sub_inst: summary of errors related to sub_elements
56 error_summary_inst: summary of errors related to elements
57 """
58 paths = self.paths
59 for ifc_file in ifc_files:
60 # Reset class based on domain to run the right check.
61 # Not pretty but works. This might be refactored in #170
62 if ifc_file.domain == IFCDomain.hydraulic:
63 self.logger.info(f"Processing HVAC-IfcCheck") # todo
64 self.__class__ = CheckIfcHVAC
65 self.__class__.__init__(self, self.playground)
66 self.paths = paths
67 elif ifc_file.domain == IFCDomain.arch:
68 self.logger.info(f"Processing BPS-IfcCheck") # todo
69 self.__class__ = CheckIfcBPS
70 self.__class__.__init__(self, self.playground)
71 self.paths = paths
72 elif ifc_file.domain == IFCDomain.unknown:
73 self.logger.info(f"No domain specified for ifc file "
74 f"{ifc_file.ifc_file_name}, not processing "
75 f"any checks")
76 return
77 else:
78 self.logger.info(
79 f"For the Domain {ifc_file.domain} no specific checks are"
80 f" implemented currently. Just running the basic checks."
81 f"")
82 self.__class__ = CheckIfc
83 self.ps_summary = self._get_class_property_sets(self.plugin)
84 self.ifc_units = ifc_file.ifc_units
85 self.sub_inst = ifc_file.file.by_type(self.sub_inst_cls)
86 self.elements = self.get_relevant_elements(ifc_file.file)
87 self.id_list = [e.GlobalId for e in ifc_file.file.by_type("IfcRoot")]
88 self.check_critical_errors(ifc_file.file, self.id_list)
89 self.error_summary_sub_inst = self.check_inst(
90 self.validate_sub_inst, self.sub_inst)
91 self.error_summary_inst = self.check_inst(
92 self.validate_elements, self.elements)
93 instance_errors = sum(len(errors) for errors in
94 self.error_summary_inst.values())
95 quality_logger = logging.getLogger('bim2sim.QualityReport')
96 quality_logger.warning(
97 '%d errors were found on %d elements' %
98 (instance_errors, len(self.error_summary_inst)))
99 sub_inst_errors = sum(len(errors) for errors in list(
100 self.error_summary_sub_inst.values()))
101 quality_logger.warning(
102 '%d errors were found on %d sub_elements' % (
103 sub_inst_errors, len(self.error_summary_sub_inst)))
104 base_name = f"/{ifc_file.domain.name.upper()}_" \
105 f"{ifc_file.ifc_file_name[:-4]}"
106 self._write_errors_to_json(base_name)
107 self._write_errors_to_html_table(base_name, ifc_file.domain)
109 def check_critical_errors(self, ifc: file, id_list: list):
110 """
111 Checks for critical errors in the IFC file.
113 Args:
114 ifc: ifc file loaded with IfcOpenShell
115 id_list: list of all GUID's in IFC File
116 Raises:
117 TypeError: if a critical error is found
118 """
119 self.check_ifc_version(ifc)
120 self.check_critical_uniqueness(id_list)
122 @staticmethod
123 def check_ifc_version(ifc: file):
124 """
125 Checks the IFC version.
127 Only IFC4 files are valid for bim2sim.
129 Args:
130 ifc: ifc file loaded with IfcOpenShell
131 Raises:
132 TypeError: if loaded IFC is not IFC4
133 """
134 schema = ifc.schema
135 if "IFC4" not in schema:
136 raise TypeError(f"Loaded IFC file is of type {schema} but only IFC4"
137 f"is supported. Please ask the creator of the model"
138 f" to provide a valid IFC4 file.")
140 @staticmethod
141 def _get_ifc_type_classes(plugin: types.ModuleType):
142 """
143 Gets all the classes of a plugin, that represent an IFCProduct,
144 and organize them on a dictionary for each ifc_type
145 Args:
146 plugin: plugin used in the check tasks (bps or hvac)
148 Returns:
149 cls_summary: dictionary containing all the ifc_types on the
150 plugin with the corresponding class
151 """
152 plugin_classes = [plugin_class[1] for plugin_class in
153 inspect.getmembers(plugin, inspect.isclass) if
154 inspect.getmro(plugin_class[1])[1].__name__.endswith(
155 'Product')]
156 cls_summary = {}
158 for plugin_class in plugin_classes:
159 # class itself
160 if plugin_class.ifc_types:
161 for ifc_type in plugin_class.ifc_types.keys():
162 cls_summary[ifc_type] = plugin_class
163 # sub classes
164 for subclass in all_subclasses(plugin_class):
165 for ifc_type in subclass.ifc_types.keys():
166 cls_summary[ifc_type] = subclass
167 return cls_summary
169 @classmethod
170 def _get_class_property_sets(cls, plugin: types.ModuleType) -> Dict:
171 """
172 Gets all property sets and properties required for bim2sim for all
173 classes of a plugin, that represent an IFCProduct, and organize them on
174 a dictionary for each ifc_type
175 Args:
176 plugin: plugin used in the check tasks (bps or hvac)
178 Returns:
179 ps_summary: dictionary containing all the ifc_types on the
180 plugin with the corresponding property sets
181 """
182 ps_summary = {}
183 cls_summary = cls._get_ifc_type_classes(plugin)
184 for ifc_type, plugin_class in cls_summary.items():
185 attributes = inspect.getmembers(
186 plugin_class, lambda a: isinstance(a, attribute.Attribute))
187 ps_summary[ifc_type] = {}
188 for attr in attributes:
189 if attr[1].default_ps:
190 ps_summary[ifc_type][attr[0]] = attr[1].default_ps
191 return ps_summary
193 def get_relevant_elements(self, ifc: file):
194 """
195 Gets all relevant ifc elements based on the plugin's classes that
196 represent an IFCProduct
198 Args:
199 ifc: IFC file translated with ifcopenshell
201 Returns:
202 ifc_elements: list of IFC instance (Products)
204 """
205 relevant_ifc_types = list(self.ps_summary.keys())
206 ifc_elements = []
207 for ifc_type in relevant_ifc_types:
208 ifc_elements.extend(ifc.by_type(ifc_type))
209 return ifc_elements
211 @staticmethod
212 def check_inst(validation_function: Callable, elements: list):
213 """
214 Uses sb_validation/ports/elements functions in order to check each
215 one and adds error to dictionary if object has errors.
216 Args:
217 validation_function: function that compiles all the validations
218 to be performed on the object (sb/port/instance)
219 elements: list containing all objects to be evaluates
221 Returns:
222 summary: summarized dictionary of errors, where the key is the
223 GUID + the ifc_type
225 """
226 summary = {}
227 for inst in elements:
228 error = validation_function(inst)
229 if len(error) > 0:
230 if hasattr(inst, 'GlobalId'):
231 key = inst.GlobalId + ' ' + inst.is_a()
232 else:
233 key = inst.is_a()
234 summary.update({key: error})
235 return summary
237 def validate_sub_inst(self, sub_inst: list) -> list:
238 raise NotImplementedError
240 def validate_elements(self, inst: list) -> list:
241 raise NotImplementedError
243 @staticmethod
244 def apply_validation_function(fct: bool, err_name: str, error: list):
245 """
246 Function to apply a validation to an instance, space boundary or
247 port, it stores the error to the list of errors.
249 Args:
250 fct: validation function to be applied
251 err_name: string that define the error
252 error: list of errors
254 """
255 if not fct:
256 error.append(err_name)
258 def _write_errors_to_json(self, base_name: str):
259 """
260 Function to write the resulting list of errors to a .json file as a
261 summary.
263 Args:ps
264 base_name: str of file base name for reports
266 """
267 with open(str(self.paths.log) +
268 base_name +
269 f"_sub_inst_error_summary.json",
270 'w+') as fp:
271 json.dump(self.error_summary_sub_inst, fp, indent="\t")
272 with open(str(self.paths.log) +
273 base_name +
274 f"_inst_error_summary.json",
275 'w+') as fp:
276 json.dump(self.error_summary_inst, fp, indent="\t")
278 @staticmethod
279 def _categorize_errors(error_dict: dict):
280 """
281 categorizes the resulting errors in a dictionary containing two groups:
282 'per_error' where the key is the error name and the value is the
283 number of errors with this name
284 'per type' where the key is the ifc_type and the values are the
285 each element with its respective errors
286 Args:
287 error_dict: dictionary containing all errors without categorization
289 Returns:
290 categorized_dict: dictionary containing all errors categorized
292 """
293 categorized_dict = {'per_error': {}, 'per_type': {}}
294 for instance, errors in error_dict.items():
295 if ' ' in instance:
296 guid, ifc_type = instance.split(' ')
297 else:
298 guid = '-'
299 ifc_type = instance
300 if ifc_type not in categorized_dict['per_type']:
301 categorized_dict['per_type'][ifc_type] = {}
302 categorized_dict['per_type'][ifc_type][guid] = errors
303 for error in errors:
304 error_com = error.split(' - ')
305 if error_com[0] not in categorized_dict['per_error']:
306 categorized_dict['per_error'][error_com[0]] = 0
307 categorized_dict['per_error'][error_com[0]] += 1
308 return categorized_dict
310 # general check functions
311 @staticmethod
312 def _check_unique(inst: entity_instance, id_list: list):
313 """
314 Check that the global id (GUID) is unique for the analyzed instance
316 Args:
317 inst: IFC instance
318 id_list: list of all GUID's in IFC File
319 Returns:
320 True: if check succeeds
321 False: if check fails
322 """
323 # Materials have no GlobalId
324 blacklist = [
325 'IfcMaterialLayer', 'IfcMaterialLayer', 'IfcMaterialLayerSet'
326 ]
327 if inst.is_a() in blacklist:
328 return True
329 return id_list.count(inst.GlobalId) == 1
331 @staticmethod
332 def check_critical_uniqueness(id_list: list):
333 """
334 Checks if all GlobalIds are unique.
336 Only files containing unique GUIDs are valid for bim2sim.
338 Args:
339 id_list: list of all GUID's in IFC File
340 Raises:
341 TypeError: if loaded file does not have unique GUIDs
342 Warning: if uppercase GUIDs are equal
343 """
344 if len(id_list) > len(set(id_list)):
345 raise TypeError(
346 f"The GUIDs of the loaded IFC file are not uniquely defined"
347 f" but files containing unique GUIDs can be used. Please ask "
348 f"the creator of the model to provide a valid IFC4 "
349 f"file.")
350 ids_upper = list(map(lambda x: x.upper(), id_list))
351 if len(ids_upper) > len(set(ids_upper)):
352 warnings.warn(
353 "Uppercase GUIDs are not uniquely defined. A restart using the"
354 "option of generating new GUIDs should be considered.")
356 def _check_inst_properties(self, inst: entity_instance):
357 """
358 Check that an instance has the property sets and properties
359 necessaries to the plugin.
361 Args:
362 inst: IFC instance
364 Returns:
365 True: if check succeeds
366 False: if check fails
367 """
368 inst_prop2check = self.ps_summary.get(inst.is_a(), {})
369 inst_prop = get_property_sets(inst, self.ifc_units)
370 inst_prop_errors = []
371 for prop2check, ps2check in inst_prop2check.items():
372 ps = inst_prop.get(ps2check[0], None)
373 if ps:
374 if not ps.get(ps2check[1], None):
375 inst_prop_errors.append(
376 prop2check+' - '+', '.join(ps2check))
377 else:
378 inst_prop_errors.append(prop2check+' - '+', '.join(ps2check))
379 if inst_prop_errors:
380 key = inst.GlobalId + ' ' + inst.is_a()
381 self.error_summary_prop.update({key: inst_prop_errors})
382 return False
383 return True
385 @staticmethod
386 def _check_inst_representation(inst: entity_instance):
387 """
388 Check that an instance has a correct geometric representation.
390 Args:
391 inst: IFC instance
393 Returns:
394 True: if check succeeds
395 False: if check fails
396 """
397 if hasattr(inst, 'Representation'):
398 return inst.Representation is not None
399 else:
400 return False
402 def get_html_templates(self):
403 """
404 Gets all stored html templates that will be used to export the errors
405 summaries
407 Returns:
408 templates: dictionary containing all error html templates
409 """
410 templates = {}
411 path_templates = os.path.join(
412 self.paths.assets, "templates", "check_ifc")
413 lookup = TemplateLookup(directories=[path_templates])
414 templates["inst_template"] = Template(
415 filename=os.path.join(path_templates, "inst_template"),
416 lookup=lookup)
417 templates["prop_template"] = Template(
418 filename=os.path.join(path_templates, "prop_template"),
419 lookup=lookup)
420 templates["summary_template"] = Template(
421 filename=os.path.join(path_templates, "summary_template"),
422 lookup=lookup)
423 return templates
425 def _write_errors_to_html_table(self, base_name: str, domain: IFCDomain):
426 """
427 Writes all errors in the html templates in a summarized way
429 Args:
430 base_name: str of file base name for reports
431 domain: IFCDomain of the checked IFC
432 """
434 templates = self.get_html_templates()
435 summary_inst = self._categorize_errors(self.error_summary_inst)
436 summary_sbs = self._categorize_errors(self.error_summary_sub_inst)
437 summary_props = self._categorize_errors(self.error_summary_prop)
438 all_errors = {**summary_inst['per_type'], **summary_sbs['per_type']}
440 with open(str(self.paths.log) +
441 base_name +
442 '_error_summary_inst.html', 'w+') as \
443 out_file:
444 out_file.write(templates["inst_template"].render_unicode(
445 task=self,
446 summary_inst=summary_inst,
447 summary_sbs=summary_sbs,
448 all_errors=all_errors))
449 out_file.close()
450 with open(str(self.paths.log) +
451 base_name +
452 '_error_summary_prop.html', 'w+') as \
453 out_file:
454 out_file.write(templates["prop_template"].render_unicode(
455 task=self,
456 summary_props=summary_props))
457 out_file.close()
458 with open(str(self.paths.log) +
459 base_name +
460 '_error_summary.html', 'w+') as out_file:
461 out_file.write(templates["summary_template"].render_unicode(
462 task=self,
463 plugin_name=domain.name.upper(),
464 base_name=base_name[1:],
465 summary_inst=summary_inst,
466 summary_sbs=summary_sbs,
467 summary_props=summary_props))
468 out_file.close()
471class CheckIfcHVAC(CheckIfc):
472 """
473 Check an IFC file for a number of conditions (missing information, incorrect information, etc) that could lead on
474 future tasks to fatal errors.
475 """
477 def __init__(self, playground: Playground):
478 super().__init__(playground)
479 self.sub_inst_cls = 'IfcDistributionPort'
480 self.plugin = hvac
482 def validate_sub_inst(self, port: entity_instance) -> list:
483 """
484 Validation function for a port that compiles all validation functions.
486 Args:
487 port: IFC port entity
489 Returns:
490 error: list of errors found in the IFC port
492 """
493 error = []
494 self.apply_validation_function(self._check_unique(port, self.id_list),
495 'GlobalId - '
496 'The space boundary GlobalID is not '
497 'unique', error)
498 self.apply_validation_function(self._check_flow_direction(port),
499 'FlowDirection - '
500 'The port flow direction is missing', error)
501 self.apply_validation_function(self._check_assignments(port),
502 'Assignments - '
503 'The port assignments are missing', error)
504 self.apply_validation_function(self._check_connection(port),
505 'Connections - '
506 'The port has no connections', error)
507 self.apply_validation_function(self._check_contained_in(port),
508 'ContainedIn - '
509 'The port is not contained in', error)
511 return error
513 def validate_elements(self, inst: entity_instance) -> list:
514 """
515 Validation function for an instance that compiles all instance validation functions.
517 Args:
518 inst: IFC instance being checked
520 Returns:
521 error: list of elements error
523 """
524 error = []
525 self.apply_validation_function(self._check_unique(inst, self.id_list),
526 'GlobalId - '
527 'The instance GlobalID is not unique', error)
528 self.apply_validation_function(self._check_inst_ports(inst),
529 'Ports - '
530 'The instance ports are missing', error)
531 self.apply_validation_function(self._check_contained_in_structure(inst),
532 'ContainedInStructure - '
533 'The instance is not contained in any '
534 'structure', error)
535 self.apply_validation_function(self._check_inst_properties(inst),
536 'Missing Property_Sets - '
537 'One or more instance\'s necessary '
538 'property sets are missing', error)
539 self.apply_validation_function(self._check_inst_representation(inst),
540 'Representation - '
541 'The instance has no geometric '
542 'representation', error)
543 self.apply_validation_function(self._check_assignments(inst),
544 'Assignments - '
545 'The instance assignments are missing', error)
547 return error
549 @staticmethod
550 def _check_flow_direction(port: entity_instance) -> bool:
551 """
552 Check that the port has a defined flow direction.
554 Args:
555 port: port IFC entity
557 Returns:
558 True if check succeeds, False otherwise
559 """
560 return port.FlowDirection in ['SOURCE', 'SINK', 'SINKANDSOURCE',
561 'SOURCEANDSINK']
563 @staticmethod
564 def _check_assignments(port: entity_instance) -> bool:
565 """
566 Check that the port has at least one assignment.
568 Args:
569 port: port ifc entity
571 Returns:
572 True: if check succeeds
573 False: if check fails
574 """
575 return any(assign.is_a('IfcRelAssignsToGroup') for assign in
576 port.HasAssignments)
578 @staticmethod
579 def _check_connection(port: entity_instance) -> bool:
580 """
581 Check that the port is: "connected_to" or "connected_from".
583 Args:
584 port: port ifc entity
586 Returns:
587 True: if check succeeds
588 False: if check fails
589 """
590 return len(port.ConnectedTo) > 0 or len(port.ConnectedFrom) > 0
592 @staticmethod
593 def _check_contained_in(port: entity_instance) -> bool:
594 """
595 Check that the port is "contained_in".
597 Args:
598 port: port ifc entity
600 Returns:
601 True: if check succeeds
602 False: if check fails
603 """
604 return len(port.ContainedIn) > 0
606 # elements check
607 @staticmethod
608 def _check_inst_ports(inst: entity_instance) -> bool:
609 """
610 Check that an instance has associated ports.
612 Args:
613 inst: IFC instance
615 Returns:
616 True: if check succeeds
617 False: if check fails
618 """
619 ports = get_ports(inst)
620 if ports:
621 return True
622 else:
623 return False
625 @staticmethod
626 def _check_contained_in_structure(inst: entity_instance) -> bool:
627 """
628 Check that an instance is contained in an structure.
630 Args:
631 inst: IFC instance
633 Returns:
634 True: if check succeeds
635 False: if check fails
636 """
637 if hasattr(inst, 'ContainedInStructure'):
638 return len(inst.ContainedInStructure) > 0
639 else:
640 return False
643class CheckIfcBPS(CheckIfc):
644 """
645 Check an IFC file, for a number of conditions (missing information,
646 incorrect information, etc.) that could lead on future tasks to
647 fatal errors.
648 """
650 def __init__(self, playground: Playground, ):
651 super().__init__(playground)
652 self.sub_inst_cls = 'IfcRelSpaceBoundary'
653 self.plugin = bps
654 self.space_indicator = True
656 def check_critical_errors(self, ifc: file, id_list: list):
657 """
658 Checks for critical errors in the IFC file.
660 Args:
661 ifc: ifc file loaded with IfcOpenShell
662 id_list: list of all GUID's in IFC File
663 Raises:
664 TypeError: if a critical error is found
665 """
666 self.check_ifc_version(ifc)
667 self.check_critical_uniqueness(id_list)
668 self.check_sub_inst_exist()
669 self.check_rel_space_exist()
671 def check_sub_inst_exist(self):
672 """
673 Checks for the existence of IfcRelSpaceBoundaries.
675 Only files containing elements of type 'IfcRelSpaceBoundary' are
676 valid for bim2sim.
678 Raises:
679 TypeError: if loaded file does not contain IfcRelSpaceBoundaries
680 """
681 if len(self.sub_inst) == 0:
682 raise TypeError(
683 f"Loaded IFC file does not contain elements of type "
684 f"'IfcRelSpaceBoundary' but only files containing "
685 f"IfcRelSpaceBoundaries can be validated. Please ask the "
686 f"creator of the model to provide a valid IFC4 file.")
688 def check_rel_space_exist(self):
689 """
690 Checks for the existence of RelatedSpace attribute of
691 IfcRelSpaceBoundaries.
693 Only IfcRelSpaceBoundaries with an IfcSpace or
694 IfcExternalSpatialElement are valid for bim2sim.
696 Raises:
697 TypeError: if loaded file only contain IfcRelSpaceBoundaries
698 without a valid RelatedSpace.
699 """
700 indicator = False
701 for inst in self.sub_inst:
702 if inst.RelatingSpace is not None:
703 indicator = True
704 break
705 if not indicator:
706 raise TypeError(
707 f"Loaded IFC file does only contain IfcRelSpaceBoundaries "
708 f"that do not have an IfcSpace or IfcExternalSpatialElement "
709 f"as RelatedSpace but those are necessary for further "
710 f"calculations. Please ask the creator of the model to provide"
711 f" a valid IFC4 file.")
713 def validate_sub_inst(self, bound: entity_instance) -> list:
714 """
715 Validation function for a space boundary that compiles all validation
716 functions.
718 Args:
719 bound: ifc space boundary entity
721 Returns:
722 error: list of errors found in the ifc space boundaries
723 """
724 error = []
725 self.apply_validation_function(self._check_unique(bound, self.id_list),
726 'GlobalId - '
727 'The space boundary GlobalID is not '
728 'unique',
729 error)
730 self.apply_validation_function(self._check_level(bound),
731 '2ndLevel - '
732 'The space boundary is not 2nd level',
733 error)
734 self.apply_validation_function(self._check_description(bound),
735 'Description - '
736 'The space boundary description does '
737 'not provide level information',
738 error)
739 self.apply_validation_function(self._check_rel_space(bound),
740 'RelatingSpace - '
741 'The space boundary does not have a '
742 'relating space associated', error)
743 self.apply_validation_function(self._check_rel_building_elem(bound),
744 'RelatedBuildingElement - '
745 'The space boundary does not have a '
746 'related building element associated',
747 error)
748 self.apply_validation_function(self._check_conn_geom(bound),
749 'ConnectionGeometry - '
750 'The space boundary does not have a '
751 'connection geometry', error)
752 self.apply_validation_function(self._check_phys_virt_bound(bound),
753 'PhysicalOrVirtualBoundary - '
754 'The space boundary is neither '
755 'physical or virtual', error)
756 self.apply_validation_function(self._check_int_ext_bound(bound),
757 'InternalOrExternalBoundary - '
758 'The space boundary is neither '
759 'external or internal', error)
760 self.apply_validation_function(self._check_on_relating_elem(bound),
761 'SurfaceOnRelatingElement - '
762 'The space boundary does not have a '
763 'surface on the relating element', error)
764 self.apply_validation_function(self._check_on_related_elem(bound),
765 'SurfaceOnRelatedElement - '
766 'The space boundary does not have a '
767 'surface on the related element', error)
768 self.apply_validation_function(self._check_basis_surface(bound),
769 'BasisSurface - '
770 'The space boundary surface on '
771 'relating element geometry is missing',
772 error)
773 self.apply_validation_function(self._check_inner_boundaries(bound),
774 'InnerBoundaries - '
775 'The space boundary surface on '
776 'relating element inner boundaries are '
777 'missing', error)
778 if hasattr(
779 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary,
780 'Segments'):
781 self.apply_validation_function(
782 self._check_outer_boundary_composite(bound),
783 'OuterBoundary - '
784 'The space boundary surface on relating element outer '
785 'boundary is missing', error)
786 self.apply_validation_function(self._check_segments(bound),
787 'OuterBoundary Segments - '
788 'The space boundary surface on '
789 'relating element outer boundary '
790 'geometry is missing', error)
791 self.apply_validation_function(self._check_segments_poly(bound),
792 'OuterBoundary SegmentsPolyline - '
793 'The space boundary surface on '
794 'relating element outer boundary '
795 'geometry is not well structured',
796 error)
797 self.apply_validation_function(
798 self._check_segments_poly_coord(bound),
799 'OuterBoundary Coordinates - '
800 'The space boundary surface on relating element outer boundary '
801 'coordinates are missing', error)
802 else:
803 self.apply_validation_function(
804 self._check_outer_boundary_poly(bound),
805 'OuterBoundary - '
806 'The space boundary surface on relating element outer boundary '
807 'is missing', error)
808 self.apply_validation_function(
809 self._check_outer_boundary_poly_coord(bound),
810 'OuterBoundary Coordinates - '
811 'The space boundary surface on relating element outer boundary '
812 'coordinates are missing', error)
814 self.apply_validation_function(self._check_plane_position(bound),
815 'Position - '
816 'The space boundary surface on relating '
817 'element plane position is missing',
818 error)
819 self.apply_validation_function(self._check_location(bound),
820 'Location - '
821 'The space boundary surface on relating '
822 'element location is missing', error)
823 self.apply_validation_function(self._check_axis(bound),
824 'Axis - '
825 'The space boundary surface on relating '
826 'element axis are missing',
827 error)
828 self.apply_validation_function(self._check_refdirection(bound),
829 'RefDirection - '
830 'The space boundary surface on relating '
831 'element reference direction is '
832 'missing', error)
833 self.apply_validation_function(self._check_location_coord(bound),
834 'LocationCoordinates - '
835 'The space boundary surface on relating '
836 'element location coordinates are '
837 'missing', error)
838 self.apply_validation_function(self._check_axis_dir_ratios(bound),
839 'AxisDirectionRatios - '
840 'The space boundary surface on relating '
841 'element axis direction ratios are '
842 'missing', error)
843 self.apply_validation_function(
844 self._check_refdirection_dir_ratios(bound),
845 'RefDirectionDirectionRatios - '
846 'The space boundary surface on relating element position '
847 'reference direction is missing', error)
849 return error
851 def validate_elements(self, inst: entity_instance) -> list:
852 """
853 Validation function for an instance that compiles all instance
854 validation functions.
856 Args:
857 inst:IFC instance being checked
859 Returns:
860 error: list of elements error
862 """
863 error = []
864 self.apply_validation_function(self._check_unique(inst, self.id_list),
865 'GlobalId - '
866 'The instance GlobalID is not unique'
867 , error)
868 self.apply_validation_function(self._check_inst_sb(inst),
869 'SpaceBoundaries - '
870 'The instance space boundaries are '
871 'missing', error)
872 self.apply_validation_function(self._check_inst_materials(inst),
873 'MaterialLayers - '
874 'The instance materials are missing',
875 error)
876 self.apply_validation_function(self._check_inst_properties(inst),
877 'Missing Property_Sets - '
878 'One or more instance\'s necessary '
879 'property sets are missing', error)
880 self.apply_validation_function(self._check_inst_contained_in_structure(inst),
881 'ContainedInStructure - '
882 'The instance is not contained in any '
883 'structure', error)
884 self.apply_validation_function(self._check_inst_representation(inst),
885 'Representation - '
886 'The instance has no geometric '
887 'representation', error)
888 return error
890 @staticmethod
891 def _check_level(bound: entity_instance):
892 """
893 Check that the space boundary is of the second level type
895 Args:
896 bound: Space boundary IFC instance
898 Returns:
899 True: if check succeeds
900 False: if check fails
901 """
902 return bound.Name == "2ndLevel"
904 @staticmethod
905 def _check_description(bound: entity_instance):
906 """
907 Check that the space boundary description is 2a or 2b
909 Args:
910 bound: Space boundary IFC instance
912 Returns:
913 True: if check succeeds
914 False: if check fails
915 """
916 return bound.Description in {'2a', '2b'}
918 @staticmethod
919 def _check_rel_space(bound: entity_instance):
920 """
921 Check that the space boundary relating space exists and has the
922 correct class.
924 Args:
925 bound: Space boundary IFC instance
927 Returns:
928 True: if check succeeds
929 False: if check fails
930 """
931 return any(
932 [bound.RelatingSpace.is_a('IfcSpace') or
933 bound.RelatingSpace.is_a('IfcExternalSpatialElement')])
935 @staticmethod
936 def _check_rel_building_elem(bound: entity_instance):
937 """
938 Check that the space boundary related building element exists and has
939 the correct class.
941 Args:
942 bound: Space boundary IFC instance
944 Returns:
945 True: if check succeeds
946 False: if check fails
947 """
948 if bound.RelatedBuildingElement is not None:
949 return bound.RelatedBuildingElement.is_a('IfcElement')
951 @staticmethod
952 def _check_conn_geom(bound: entity_instance):
953 """
954 Check that the space boundary has a connection geometry and has the
955 correct class.
957 Args:
958 bound: Space boundary IFC instance
960 Returns:
961 True: if check succeeds
962 False: if check fails
963 """
964 return bound.ConnectionGeometry.is_a('IfcConnectionGeometry')
966 @staticmethod
967 def _check_phys_virt_bound(bound: entity_instance):
968 """
969 Check that the space boundary is virtual or physical.
971 Args:
972 bound: Space boundary IFC instance
974 Returns:
975 True: if check succeeds
976 False: if check fails
977 """
978 return bound.PhysicalOrVirtualBoundary.upper() in \
979 {'PHYSICAL', 'VIRTUAL', 'NOTDEFINED'}
981 @staticmethod
982 def _check_int_ext_bound(bound: entity_instance):
983 """
984 Check that the space boundary is internal or external.
986 Args:
987 bound: Space boundary IFC instance
989 Returns:
990 True: if check succeeds
991 False: if check fails
992 """
993 return bound.InternalOrExternalBoundary.upper() in {'INTERNAL',
994 'EXTERNAL',
995 'EXTERNAL_EARTH',
996 'EXTERNAL_FIRE',
997 'EXTERNAL_WATER'
998 }
1000 @staticmethod
1001 def _check_on_relating_elem(bound: entity_instance):
1002 """
1003 Check that the surface on relating element of a space boundary has
1004 the geometric information.
1006 Args:
1007 bound: Space boundary IFC instance
1009 Returns:
1010 True: if check succeeds
1011 False: if check fails
1012 """
1013 return bound.ConnectionGeometry.SurfaceOnRelatingElement.is_a(
1014 'IfcCurveBoundedPlane')
1016 @staticmethod
1017 def _check_on_related_elem(bound: entity_instance):
1018 """
1019 Check that the surface on related element of a space boundary has no
1020 geometric information.
1022 Args:
1023 bound: Space boundary IFC instance
1025 Returns:
1026 True: if check succeeds
1027 False: if check fails
1028 """
1029 return (bound.ConnectionGeometry.SurfaceOnRelatedElement is None or
1030 bound.ConnectionGeometry.SurfaceOnRelatedElement.is_a(
1031 'IfcCurveBoundedPlane'))
1033 @staticmethod
1034 def _check_basis_surface(bound: entity_instance):
1035 """
1036 Check that the surface on relating element of a space boundary is
1037 represented by an IFC Place.
1039 Args:
1040 bound: Space boundary IFC instance
1042 Returns:
1043 True: if check succeeds
1044 False: if check fails
1045 """
1046 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \
1047 BasisSurface.is_a('IfcPlane')
1049 @staticmethod
1050 def _check_inner_boundaries(bound: entity_instance):
1051 """
1052 Check if the surface on relating element of a space boundary inner
1053 boundaries don't exists or are composite curves.
1055 Args:
1056 bound: Space boundary IFC instance
1058 Returns:
1059 True: if check succeeds
1060 False: if check fails
1061 """
1062 return (bound.ConnectionGeometry.SurfaceOnRelatingElement.
1063 InnerBoundaries is None) or \
1064 (i.is_a('IfcCompositeCurve') for i in bound.ConnectionGeometry.
1065 SurfaceOnRelatingElement.InnerBoundaries)
1067 @staticmethod
1068 def _check_outer_boundary_composite(bound: entity_instance):
1069 """
1070 Check if the surface on relating element of a space boundary outer
1071 boundaries are composite curves.
1073 Args:
1074 bound: Space boundary IFC instance
1076 Returns:
1077 True: if check succeeds
1078 False: if check fails
1079 """
1080 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \
1081 OuterBoundary.is_a('IfcCompositeCurve')
1083 @staticmethod
1084 def _check_segments(bound: entity_instance):
1085 """
1086 Check if the surface on relating element of a space boundary outer
1087 boundaries segments are polyline.
1089 Args:
1090 bound: Space boundary IFC instance
1092 Returns:
1093 True: if check succeeds
1094 False: if check fails
1095 """
1096 return (s.is_a('IfcCompositeCurveSegment') for s in
1097 bound.ConnectionGeometry.SurfaceOnRelatingElement.
1098 OuterBoundary.Segments)
1100 @classmethod
1101 def _check_segments_poly(cls, bound: entity_instance):
1102 """
1103 Check segments of an outer boundary of a surface on relating element.
1105 Args:
1106 bound: Space boundary IFC instance
1108 Returns:
1109 True: if check succeeds
1110 False: if check fails
1111 """
1112 return all(cls._check_poly_points(s.ParentCurve)
1113 for s in
1114 bound.ConnectionGeometry.SurfaceOnRelatingElement
1115 .OuterBoundary.Segments)
1117 @classmethod
1118 def _check_segments_poly_coord(cls, bound: entity_instance):
1119 """
1120 Check segments coordinates of an outer boundary of a surface on
1121 relating element.
1123 Args:
1124 bound: Space boundary IFC instance
1126 Returns:
1127 True: if check succeeds
1128 False: if check fails
1129 """
1130 return all(cls._check_poly_points_coord(s.ParentCurve)
1131 for s in
1132 bound.ConnectionGeometry.SurfaceOnRelatingElement.
1133 OuterBoundary.Segments)
1135 @classmethod
1136 def _check_outer_boundary_poly(cls, bound: entity_instance):
1137 """
1138 Check points of outer boundary of a surface on relating element.
1140 Args:
1141 bound: Space boundary IFC instance
1143 Returns:
1144 True: if check succeeds
1145 False: if check fails
1146 """
1147 return cls._check_poly_points(
1148 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary)
1150 @staticmethod
1151 def _check_outer_boundary_poly_coord(bound: entity_instance):
1152 """
1153 Check outer boundary of a surface on relating element.
1155 Args:
1156 bound: Space boundary IFC instance
1158 Returns:
1159 True: if check succeeds
1160 False: if check fails
1161 """
1162 return all(
1163 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary)
1165 @staticmethod
1166 def _check_plane_position(bound: entity_instance):
1167 """
1168 Check class of plane position of space boundary.
1170 Args:
1171 bound: Space boundary IFC instance
1173 Returns:
1174 True: if check succeeds
1175 False: if check fails
1176 """
1177 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
1178 Position.is_a('IfcAxis2Placement3D')
1180 @staticmethod
1181 def _check_location(bound: entity_instance):
1182 """
1183 Check that location of a space boundary is an IfcCartesianPoint.
1185 Args:
1186 bound: Space boundary IFC instance
1188 Returns:
1189 True: if check succeeds
1190 False: if check fails
1191 """
1192 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
1193 Position.Location.is_a('IfcCartesianPoint')
1195 @staticmethod
1196 def _check_axis(bound: entity_instance):
1197 """
1198 Check that axis of space boundary is an IfcDirection.
1200 Args:
1201 bound: Space boundary IFC instance
1203 Returns:
1204 True: if check succeeds
1205 False: if check fails
1206 """
1207 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
1208 Position.Axis.is_a('IfcDirection')
1210 @staticmethod
1211 def _check_refdirection(bound: entity_instance):
1212 """
1213 Check that reference direction of space boundary is an IfcDirection.
1215 Args:
1216 bound: Space boundary IFC instance
1218 Returns:
1219 True: if check succeeds
1220 False: if check fails
1221 """
1222 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
1223 Position.RefDirection.is_a('IfcDirection')
1225 @classmethod
1226 def _check_location_coord(cls, bound: entity_instance):
1227 """
1228 Check if space boundary surface on relating element coordinates are
1229 correct.
1231 Args:
1232 bound: Space boundary IFC instance
1234 Returns:
1235 True: if check succeeds
1236 False: if check fails
1237 """
1238 return cls._check_coords(bound.ConnectionGeometry.
1239 SurfaceOnRelatingElement.BasisSurface.
1240 Position.Location)
1242 @classmethod
1243 def _check_axis_dir_ratios(cls, bound: entity_instance):
1244 """
1245 Check if space boundary surface on relating element axis are correct.
1247 Args:
1248 bound: Space boundary IFC instance
1250 Returns:
1251 True: if check succeeds
1252 False: if check fails
1253 """
1254 return cls._check_dir_ratios(
1255 bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface.
1256 Position.Axis)
1258 @classmethod
1259 def _check_refdirection_dir_ratios(cls, bound: entity_instance):
1260 """
1261 Check if space boundary surface on relating element reference direction
1262 are correct.
1264 Args:
1265 bound: Space boundary IFC instance
1267 Returns:
1268 True: if check succeeds
1269 False: if check fails
1270 """
1271 return cls._check_dir_ratios(
1272 bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface.
1273 Position.RefDirection)
1275 @staticmethod
1276 def _check_poly_points(polyline: entity_instance):
1277 """
1278 Check if a polyline has the correct class.
1280 Args:
1281 polyline: Polyline IFC instance
1283 Returns:
1284 True: if check succeeds
1285 False: if check fails
1286 """
1287 return polyline.is_a('IfcPolyline')
1289 @staticmethod
1290 def _check_coords(points: entity_instance):
1291 """
1292 Check coordinates of a group of points (class and length).
1294 Args:
1295 points: Points IFC instance
1297 Returns:
1298 True: if check succeeds
1299 False: if check fails
1300 """
1301 return points.is_a('IfcCartesianPoint') and 1 <= len(
1302 points.Coordinates) <= 4
1304 @staticmethod
1305 def _check_dir_ratios(dir_ratios: entity_instance):
1306 """
1307 Check length of direction ratios.
1309 Args:
1310 dir_ratios: direction ratios IFC instance
1312 Returns:
1313 True: if check succeeds
1314 False: if check fails
1315 """
1316 return 2 <= len(dir_ratios.DirectionRatios) <= 3
1318 @classmethod
1319 def _check_poly_points_coord(cls, polyline: entity_instance):
1320 """
1321 Check if a polyline has the correct coordinates.
1323 Args:
1324 polyline: Polyline IFC instance
1326 Returns:
1327 True: if check succeeds
1328 False: if check fails
1329 """
1330 return all(cls._check_coords(p) for p in polyline.Points)
1332 @staticmethod
1333 def _check_inst_sb(inst: entity_instance):
1334 """
1335 Check that an instance has associated space boundaries (space or
1336 building element).
1338 Args:
1339 inst: IFC instance
1341 Returns:
1342 True: if check succeeds
1343 False: if check fails
1344 """
1345 blacklist = ['IfcBuilding', 'IfcSite', 'IfcBuildingStorey',
1346 'IfcMaterial', 'IfcMaterialLayer', 'IfcMaterialLayerSet']
1347 if inst.is_a() in blacklist:
1348 return True
1349 elif inst.is_a('IfcSpace') or inst.is_a('IfcExternalSpatialElement'):
1350 return len(inst.BoundedBy) > 0
1351 else:
1352 if len(inst.ProvidesBoundaries) > 0:
1353 return True
1354 decompose = []
1355 if hasattr(inst, 'Decomposes') and len(inst.Decomposes):
1356 decompose = [decomp.RelatingObject for decomp in
1357 inst.Decomposes]
1358 elif hasattr(inst, 'IsDecomposedBy') and len(inst.IsDecomposedBy):
1359 decompose = []
1360 for decomp in inst.IsDecomposedBy:
1361 for inst_ifc in decomp.RelatedObjects:
1362 decompose.append(inst_ifc)
1363 for inst_decomp in decompose:
1364 if len(inst_decomp.ProvidesBoundaries):
1365 return True
1366 return False
1368 @staticmethod
1369 def _check_inst_materials(inst: entity_instance):
1370 """
1371 Check that an instance has associated materials.
1373 Args:
1374 inst: IFC instance
1376 Returns:
1377 True: if check succeeds
1378 False: if check fails
1379 """
1380 blacklist = [
1381 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace',
1382 'IfcExternalSpatialElement']
1383 if not (inst.is_a() in blacklist):
1384 return len(get_layers_ifc(inst)) > 0
1385 return True
1387 @staticmethod
1388 def _check_inst_contained_in_structure(inst: entity_instance):
1389 """
1390 Check that an instance is contained in an structure.
1392 Args:
1393 inst: IFC instance
1395 Returns:
1396 True: if check succeeds
1397 False: if check fails
1398 """
1399 blacklist = [
1400 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace',
1401 'IfcExternalSpatialElement', 'IfcMaterial', 'IfcMaterialLayer',
1402 'IfcMaterialLayerSet'
1403 ]
1404 if not (inst.is_a() in blacklist):
1405 return len(inst.ContainedInStructure) > 0
1406 if hasattr(inst, 'Decomposes'):
1407 return len(inst.Decomposes) > 0
1408 else:
1409 return True
1411 @staticmethod
1412 def _check_inst_representation(inst: entity_instance):
1413 """
1414 Check that an instance has a correct geometric representation.
1416 Args:
1417 inst: IFC instance
1419 Returns:
1420 True: if check succeeds
1421 False: if check fails
1422 """
1423 blacklist = [
1424 'IfcBuilding', 'IfcBuildingStorey', 'IfcMaterial',
1425 'IfcMaterialLayer', 'IfcMaterialLayerSet'
1426 ]
1427 if not (inst.is_a() in blacklist):
1428 return inst.Representation is not None
1429 return True