Coverage for bim2sim / tasks / checks / check_ifc.py: 0%
501 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 10:59 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 10:59 +0000
1"""Check ifc input file mainly based on IDS files."""
3import inspect # used for _get_ifc_type_classes
4import types # used for _get_class_property_sets
5import os
6import warnings
8from pathlib import Path
10from typing import Callable, Dict # Dict used for _get_class_property_sets
12import ifcopenshell as ifcos
13from bim2sim.utilities.common_functions import all_subclasses # used in _get_ifc_type_classes
14from bim2sim.elements.mapping import attribute # used in _get_ifc_type_classes
15# get_layer_ifc needed for _check_inst_materials
16from bim2sim.elements.mapping.ifc2python import get_layers_ifc, \
17 get_property_sets, get_ports
19import ifctester.ids
20import ifctester.reporter
21import webbrowser
24from mako.lookup import TemplateLookup
25from mako.template import Template
27from bim2sim.elements import bps_elements as bps, hvac_elements as hvac
28from bim2sim.tasks.base import ITask, Playground
30from bim2sim.kernel.ifc_file import IfcFileClass
31from bim2sim.utilities.types import IFCDomain
32from bim2sim import __file__ as bs_file
33from bim2sim.tasks.common.load_ifc import extract_ifc_file_names
36class CheckIfc(ITask):
37 """Check ifc files for their quality regarding simulation."""
39 reads = ('ifc_files',)
41 def __init__(self, playground: Playground):
42 """Initialize CheckIFC."""
43 super().__init__(playground)
44 self.error_summary_sub_inst: dict = {}
45 self.error_summary_inst: dict = {}
46 self.error_summary_prop: dict = {}
47 self.version_error: bool = False
48 self.ifc_version: str = None
49 self.all_guids_unique: bool = True
50 self.double_guids: dict = {}
51 self.all_guids_filled: bool = True
52 self.empty_guids: dict = {}
53 self.sub_inst: list = []
54 self.id_list: list = []
55 self.elements: list = []
56 self.ps_summary: dict = {}
57 self.ifc_units: dict = {}
58 self.sub_inst_cls = None
59 self.plugin = None
61 def run(self, ifc_files: [IfcFileClass]):
62 """
63 Analyzes sub_elements and elements of an IFC file.
65 Therefore validation functions check ifc files and export the errors
66 found as .html files.
68 It creates following reports:
69 error_summary: overview of all errors
70 error_summary_inst: summary of errors related to elements
71 error_summary_prop: summary of missing properties
72 error_summary_guid: summary of GUID errors
73 ifc_ids_check: results of checks based on IDS file
74 These html files are stored in the log folder of the project folder.
76 Args:
77 ifc_files: bim2sim IfcFileClass holding the ifcopenshell ifc
78 instance
79 """
80 self.logger.info("Processing IFC Checks with ifcTester")
82 base_path = self.paths.ifc_base
84 ifc_files_paths = extract_ifc_file_names(base_path)
85 self.logger.info(f"Found {len(ifc_files_paths)} IFC files in project "
86 f"directory.")
88 log_path = self.paths.log
89 # ids check call start
90 if self.playground.sim_settings.ids_file_path is None:
91 self.logger.critical("Default ids file is used, pls set " +
92 "project.sim_settings.ids_file_path!")
93 self.playground.sim_settings.ids_file_path = (
94 Path(bs_file).parent /
95 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids'
96 )
98 ids_file_path = self.playground.sim_settings.ids_file_path
99 for ifc_file_path in ifc_files_paths:
100 all_spec_pass = self.run_ids_check_on_ifc(
101 ifc_file_path, ids_file_path,
102 report_html=True, log_path=log_path)
104 if all_spec_pass:
105 self.logger.info(
106 "all checks of the specifications of this IDS pass: " +
107 "{}".format(all_spec_pass))
108 else:
109 self.logger.warning(
110 "all checks of the specifications of this IDS pass: " +
111 "{}".format(all_spec_pass))
112 # ids check call end
114 self.logger.info("Processing IFC Checks without ifcTester")
116 paths = self.paths
117 for ifc_file in ifc_files:
118 # checks are domain specific
119 # Reset class based on domain to run the right check.
120 # Not pretty but works. This might be refactored in #170
122 # check uniqueness of GUIDs
123 self.all_guids_unique, self.double_guids = (
124 CheckLogicBase.run_check_guid_unique(ifc_file)
125 )
126 list_guids_non_unique = list(self.double_guids.keys())
127 self.logger.info("the GUIDs of all elements are unique: " +
128 "{}".format(self.all_guids_unique))
129 if self.all_guids_unique is False:
130 self.logger.critical("non-unique GUIDs: " +
131 "{}".format(list_guids_non_unique))
132 # check emptiness of GUID fields
133 self.all_guids_filled, self.empty_guids = (
134 CheckLogicBase.run_check_guid_empty(ifc_file)
135 )
136 list_guids_empty = list(self.empty_guids.keys())
137 self.logger.info("the GUIDs of all elements are filled " +
138 "(NOT empty): {}".format(self.all_guids_filled))
139 if self.all_guids_filled is False:
140 self.logger.critical("empty GUIDs: {}".format(list_guids_empty))
141 # check ifc version
142 self.version_error, self.ifc_version = (
143 CheckLogicBase.run_check_ifc_version(ifc_file)
144 )
145 # for doc string
146 # Logs:
147 # critical: if loaded IFC is not IFC4
148 if self.version_error:
149 self.logger.critical("ifc Version is not fitting. " +
150 "Should be IFC4, but here: " +
151 self.ifc_version)
153 if ifc_file.domain == IFCDomain.hydraulic:
154 self.logger.info("Processing HVAC-IfcCheck")
155 # used for data preparing, it is a filter keyword
156 self.sub_inst_cls = 'IfcDistributionPort'
157 self.plugin = hvac
158 self.ps_summary = self._get_class_property_sets(self.plugin)
159 self.sub_inst = ifc_file.file.by_type(self.sub_inst_cls)
160 self.elements = self.get_relevant_elements(ifc_file.file)
161 self.ifc_units = ifc_file.ifc_units
162 # checking itself
163 check_logic_hvac = CheckLogicHVAC(
164 self.sub_inst, self.elements, self.ps_summary,
165 self.ifc_units)
166 self.error_summary_sub_inst = check_logic_hvac.check_inst_sub()
167 self.error_summary_inst = check_logic_hvac.check_elements()
169 elif ifc_file.domain == IFCDomain.arch:
170 self.logger.info("Processing BPS-IfcCheck")
171 # used for data preparing, it is a filter keyword
172 self.sub_inst_cls = 'IfcRelSpaceBoundary'
173 self.plugin = bps
174 self.ps_summary = self._get_class_property_sets(self.plugin)
175 self.sub_inst = ifc_file.file.by_type(self.sub_inst_cls)
176 self.elements = self.get_relevant_elements(ifc_file.file)
177 self.ifc_units = ifc_file.ifc_units
178 # checking itself
179 check_logic_bps = CheckLogicBPS(
180 self.sub_inst, self.elements, self.ps_summary,
181 self.ifc_units)
182 self.error_summary_sub_inst = check_logic_bps.check_inst_sub()
183 self.error_summary_inst = check_logic_bps.check_elements()
184 self.error_summary_prop = check_logic_bps.error_summary_prop
185 self.paths = paths
186 elif ifc_file.domain == IFCDomain.unknown:
187 self.logger.info(f"No domain specified for ifc file "
188 f"{ifc_file.ifc_file_name}, not processing "
189 f"any checks")
190 return
191 else:
192 self.logger.info(
193 f"For the Domain {ifc_file.domain} no specific checks are"
194 f" implemented currently. Just running the basic checks."
195 f"")
197 # generating reports (of the additional checks)
198 base_name = f"/{ifc_file.domain.name.upper()}_" \
199 f"{ifc_file.ifc_file_name[:-4]}"
200 self._write_errors_to_html_table(base_name, ifc_file.domain)
202 def get_relevant_elements(self, ifc: ifcos.file):
203 """Get all relevant ifc elements.
205 This function based on the plugin's classes that
206 represent an IFCProduct.
208 Args:
209 ifc: IFC file translated with ifcopenshell
211 Returns:
212 ifc_elements: list of IFC instance (Products)
213 """
214 relevant_ifc_types = list(self.ps_summary.keys())
215 ifc_elements = []
216 for ifc_type in relevant_ifc_types:
217 ifc_elements.extend(ifc.by_type(ifc_type))
218 return ifc_elements
220 @staticmethod
221 def _get_ifc_type_classes(plugin: types.ModuleType):
222 """Get all the classes of a plugin that represent an IFCProduct.
224 Furthermore, organize them on a dictionary for each ifc_type.
225 Args:
226 plugin: plugin used in the check tasks (bps or hvac)
228 Returns:
229 cls_summary: dictionary containing all the ifc_types on the
230 plugin with the corresponding class
231 """
232 plugin_classes = [plugin_class[1] for plugin_class in
233 inspect.getmembers(plugin, inspect.isclass) if
234 inspect.getmro(plugin_class[1])[1].__name__.endswith(
235 'Product')]
236 cls_summary = {}
238 for plugin_class in plugin_classes:
239 # class itself
240 if plugin_class.ifc_types:
241 for ifc_type in plugin_class.ifc_types.keys():
242 cls_summary[ifc_type] = plugin_class
243 # sub classes
244 for subclass in all_subclasses(plugin_class):
245 for ifc_type in subclass.ifc_types.keys():
246 cls_summary[ifc_type] = subclass
247 return cls_summary
249 @classmethod
250 def _get_class_property_sets(cls, plugin: types.ModuleType) -> Dict:
251 """Get all property sets and properties.
253 Which are required for bim2sim for all classes of a plugin, that
254 represent an IFCProduct, and organize them on a dictionary for each
255 ifc_type Args: plugin: plugin used in the check tasks (bps or hvac)
257 Returns:
258 ps_summary: dictionary containing all the ifc_types on the
259 plugin with the corresponding property sets
261 """
262 ps_summary = {}
263 cls_summary = cls._get_ifc_type_classes(plugin)
264 for ifc_type, plugin_class in cls_summary.items():
265 attributes = inspect.getmembers(
266 plugin_class, lambda a: isinstance(a, attribute.Attribute))
267 ps_summary[ifc_type] = {}
268 for attr in attributes:
269 if attr[1].default_ps:
270 ps_summary[ifc_type][attr[0]] = attr[1].default_ps
271 return ps_summary
273 def validate_sub_inst(self, sub_inst: list) -> list:
274 """Raise NotImplemented Error."""
275 raise NotImplementedError
277 @staticmethod
278 def run_ids_check_on_ifc(ifc_file: str, ids_file: str,
279 report_html: bool = False,
280 log_path: str = None) -> bool:
281 """Run check on IFC file based on IDS.
283 print the check of specifications pass(true) or fail(false)
284 and the name of the specification
285 and if all specifications of one IDS pass
287 Args:
288 ifc_file: path of the IFC file, which is checked
289 ids_file: path of the IDS file, which includes the specifications
290 log_path: path of the log folder as part of the project structure
291 report_html: generate, save and open the report about checking
292 default = False
293 Returns:
294 all_spec_pass: boolean
295 (true: all specification passed,
296 false: one or more specification not passed)
297 """
298 model = ifcos.open(ifc_file)
299 my_ids = ifctester.ids.open(ids_file)
300 my_ids.validate(model)
301 all_spec_pass = True
302 for spec in my_ids.specifications:
303 if not spec.status:
304 all_spec_pass = False
306 # generate html report
307 if report_html:
308 engine = ifctester.reporter.Html(my_ids)
309 engine.report()
310 output_file = Path(log_path / 'ifc_ids_check.html')
311 engine.to_file(output_file)
312 # can comment out, if not the browser should show the report
313 # webbrowser.open(f"file://{output_file}")
315 return all_spec_pass
317 def get_html_templates(self):
318 """Get all stored html templates.
320 Which will be used to export the errors summaries.
322 Returns:
323 templates: dictionary containing all error html templates
324 """
325 templates = {}
326 path_templates = os.path.join(
327 self.paths.assets, "templates", "check_ifc")
328 lookup = TemplateLookup(directories=[path_templates])
329 templates["inst_template"] = Template(
330 filename=os.path.join(path_templates, "inst_template"),
331 lookup=lookup)
332 templates["prop_template"] = Template(
333 filename=os.path.join(path_templates, "prop_template"),
334 lookup=lookup)
335 templates["summary_template"] = Template(
336 filename=os.path.join(path_templates, "summary_template_extend"),
337 lookup=lookup)
338 templates["guid_template"] = Template(
339 filename=os.path.join(path_templates, "guid_template"),
340 lookup=lookup)
341 return templates
343 @staticmethod
344 def _categorize_errors(error_dict: dict):
345 """Categorizes the resulting errors in a dictionary.
347 This dictionary contains two groups:
348 'per_error' where the key is the error name and the value is the
349 number of errors with this name
350 'per type' where the key is the ifc_type and the values are the
351 each element with its respective errors
352 Args:
353 error_dict: dictionary containing all errors without categorization
355 Returns:
356 categorized_dict: dictionary containing all errors categorized
357 """
358 categorized_dict = {'per_error': {}, 'per_type': {}}
359 for instance, errors in error_dict.items():
360 if ' ' in instance:
361 guid, ifc_type = instance.split(' ')
362 else:
363 guid = '-'
364 ifc_type = instance
365 if ifc_type not in categorized_dict['per_type']:
366 categorized_dict['per_type'][ifc_type] = {}
367 categorized_dict['per_type'][ifc_type][guid] = errors
368 for error in errors:
369 error_com = error.split(' - ')
370 if error_com[0] not in categorized_dict['per_error']:
371 categorized_dict['per_error'][error_com[0]] = 0
372 categorized_dict['per_error'][error_com[0]] += 1
373 return categorized_dict
375 def _write_errors_to_html_table(self, base_name: str, domain: IFCDomain):
376 """Write all errors in the html templates in a summarized way.
378 Args:
379 base_name: str of file base name for reports
380 domain: IFCDomain of the checked IFC
381 """
382 show_report = False # enable the automatic popup of the reports
383 templates = self.get_html_templates()
384 summary_inst = self._categorize_errors(self.error_summary_inst)
385 summary_sbs = self._categorize_errors(self.error_summary_sub_inst)
386 summary_props = self._categorize_errors(self.error_summary_prop)
387 all_errors = {**summary_inst['per_type'], **summary_sbs['per_type']}
389 with open(str(self.paths.log) +
390 base_name +
391 '_error_summary_inst.html', 'w+') as \
392 out_file:
393 out_file.write(templates["inst_template"].render_unicode(
394 task=self,
395 summary_inst=summary_inst,
396 summary_sbs=summary_sbs,
397 all_errors=all_errors))
398 out_file.close()
399 # opens automatically browser tab showing the generated html report
400 if show_report:
401 webbrowser.open(f"file://{out_file.buffer.name}")
402 with open(str(self.paths.log) +
403 base_name +
404 '_error_summary_prop.html', 'w+') as \
405 out_file:
406 out_file.write(templates["prop_template"].render_unicode(
407 task=self,
408 summary_props=summary_props))
409 out_file.close()
410 # opens automatically browser tab showing the generated html report
411 if show_report:
412 webbrowser.open(f"file://{out_file.buffer.name}")
413 with open(str(self.paths.log) +
414 base_name +
415 '_error_summary.html', 'w+') as out_file:
416 out_file.write(templates["summary_template"].render_unicode(
417 ifc_version=self.ifc_version,
418 version_error=self.version_error,
419 all_guids_unique=self.all_guids_unique,
420 double_guids=self.double_guids,
421 all_guids_filled=self.all_guids_filled,
422 empty_guids=self.empty_guids,
423 task=self,
424 plugin_name=domain.name.upper(),
425 base_name=base_name[1:],
426 summary_inst=summary_inst,
427 summary_sbs=summary_sbs,
428 summary_props=summary_props))
429 out_file.close()
430 # opens automatically browser tab showing the generated html report
431 if show_report:
432 webbrowser.open(f"file://{out_file.buffer.name}")
434 with open(str(self.paths.log) + base_name + '_error_summary_guid.html',
435 'w+') as \
436 out_file:
437 out_file.write(templates["guid_template"].render_unicode(
438 task=self,
439 double_guids=self.double_guids,
440 empty_guids=self.empty_guids,
441 summary_inst=summary_inst,
442 summary_sbs=summary_sbs,
443 all_errors=all_errors))
444 out_file.close()
445 # opens automatically browser tab showing the generated html report
446 if show_report:
447 webbrowser.open(f"file://{out_file.buffer.name}")
450class CheckLogicBase():
451 """Provides logic for ifc files checking regarding simulation.
453 This is a base class. This base class includes all check logic, which is
454 useful for all checking use cases.
456 Attributes:
457 extract_data (list): filtered/extract data from ifc file
458 """
460 def __init__(self, extract_data, elements, ps_summary, ifc_units):
461 """Initialize class."""
462 self.space_ndicator = True
463 # filtered data, which will be processed
464 self.extract_data = extract_data
465 self.elements = elements
466 self.ps_summary = ps_summary
467 self.ifc_units = ifc_units
468 self.error_summary_prop: dict = {}
470 @staticmethod
471 def run_check_guid_unique(ifc_file) -> (bool, dict):
472 """Check the uniqueness of the guids of the IFC file.
474 Here the bijective uniqueness is check, but also
475 the uniqueness of modified guids by transforming
476 the lowercase letters into uppercase letter
477 Args:
478 ifc_file: path of the IFC file, which is checked
480 Returns:
481 all_guids_unique: boolean
482 (true: all guids are unique
483 false: one or more guids are not unique)
485 double_guid: dict
487 """
488 # dict of all elements with guids used in the checked ifc model
489 used_guids: dict[str, ifcos.entity_instance] = dict()
490 # dict of elements with guids, which are not unique
491 double_guids: dict[str, ifcos.entity_instance] = dict()
492 all_guids_unique = True
493 used_guids_upper = [] # to store temporally guid in uppercase letters
494 for inst in ifc_file.file:
495 if hasattr(inst, "GlobalId"):
496 guid = inst.GlobalId
497 # print(guid)
498 upper_guid = guid.upper()
499 # print(upper_guid)
500 if guid in used_guids:
501 double_guids[guid] = inst
502 all_guids_unique = False
503 warnings.warn(
504 "Some GUIDs are not unique! A bijective ifc file have "
505 "to have unique GUIDs. But bim2sim provides an option "
506 "in sim_settings: rest_guids = True"
507 )
508 elif (guid.upper() in used_guids_upper):
509 double_guids[guid] = inst
510 all_guids_unique = False
511 warnings.warn(
512 "Some GUIDs are not unique (for transformed GUIDS "
513 "letters low-case into uppercase)! "
514 "A bijective ifc file have "
515 "to have unique GUIDs. But bim2sim provides an option "
516 "in sim_settings: rest_guids = True"
517 )
518 else:
519 used_guids[guid] = inst
520 # store temporally guid in uppercase letters
521 used_guids_upper.append(upper_guid)
522 return (all_guids_unique, double_guids)
524 @staticmethod
525 def run_check_guid_empty(ifc_file) -> (bool, dict):
526 """Check it there is/are guid/s, which is/are empty in the IFC file.
528 Args:
529 ifc_file: path of the IFC file, which is checked
531 Returns:
532 all_guids_filled: boolean
533 (true: all guids has a value (not empty)
534 false: one or more guids has not value (empty))
536 empty_guid: dict
537 """
538 # dict of all elements with guids used in the checked ifc model
539 used_guids: dict[str, ifcos.entity_instance] = dict()
540 # dict of elements with guids, which are empty
541 empty_guids: dict[str, ifcos.entity_instance] = dict()
542 all_guids_filled = True
543 # count the number of guids without value (empty), this number is used
544 # to make unique identifier
545 guid_empty_no = 0
546 for inst in ifc_file.file:
547 if hasattr(inst, "GlobalId"):
548 guid = inst.GlobalId
549 name = inst.Name
550 if guid == '':
551 all_guids_filled = False
552 guid_empty_no = guid_empty_no + 1
553 name_dict = name + '--' + str(guid_empty_no)
554 empty_guids[name_dict] = inst
555 else:
556 used_guids[guid] = inst
557 return (all_guids_filled, empty_guids)
559 @staticmethod
560 def run_check_ifc_version(ifc: ifcos.file) -> (bool, str):
561 """Check the IFC version.
563 Only IFC4 files are valid for bim2sim.
565 Attention: no Error is raised anymore.
567 Args:
568 ifc: ifc file loaded with IfcOpenShell
569 Returns:
570 version_error: True if version NOT fit
571 ifc_version: version of the ifc file
572 """
573 schema = ifc.schema
574 if "IFC4" not in schema:
575 version_error = True
576 else:
577 version_error = False
578 return (version_error, schema)
580 def check_inst_sub(self):
581 """Check sub instances for errors.
583 Based on functions in validate_sub_inst via check_inst
584 """
585 error_summary_sub_inst = self.check_inst(
586 self.validate_sub_inst, self.extract_data)
587 return error_summary_sub_inst
589 def check_elements(self):
590 """Check elements for errors.
592 Based on functions in validate_sub_inst via check_inst
593 """
594 error_summary = self.check_inst(
595 self.validate_elements, self.elements)
596 return error_summary
598 @staticmethod
599 def check_inst(validation_function: Callable, elements: list):
600 """Check each element lists.
602 Use sb_validation/ports/elements functions to check elements
603 adds error to dictionary if object has errors. Combines the
604 (error) return of the specific validation function with the
605 key (mostly the GlobalID).
607 Args:
608 validation_function: function that compiles all the validations
609 to be performed on the object
610 (sb/port/instance)
611 elements: list containing all objects to be evaluates
613 Returns:
614 summary: summarized dictionary of errors, where the key is the
615 GUID + the ifc_type
617 """
618 summary = {}
619 for inst in elements:
620 error = validation_function(inst)
621 if len(error) > 0:
622 if hasattr(inst, 'GlobalId'):
623 key = inst.GlobalId + ' ' + inst.is_a()
624 else:
625 key = inst.is_a()
626 summary.update({key: error})
627 return summary
629 @staticmethod
630 def apply_validation_function(fct: bool, err_name: str, error: list):
631 """Apply a validation to an instance, space boundary or port.
633 Function to apply a validation to an instance, space boundary or
634 port, it stores the error to the list of errors.
636 Args:
637 fct: validation function to be applied
638 err_name: string that define the error
639 error: list of errors
641 """
642 if not fct:
643 error.append(err_name)
645 @staticmethod
646 def _check_rel_space(bound: ifcos.entity_instance):
647 """Check the existence of related space.
649 Args:
650 bound: Space boundary IFC instance
652 Returns:
653 True: if check succeeds
654 False: if check fails
655 """
656 return any(
657 [bound.RelatingSpace.is_a('IfcSpace') or
658 bound.RelatingSpace.is_a('IfcExternalSpatialElement')])
660 def validate_sub_inst(self, sub_inst: list) -> list:
661 """Raise NotImplemented Error."""
662 raise NotImplementedError
665class CheckLogicBPS(CheckLogicBase):
666 """Provides additional logic for ifc files checking regarding BPS."""
668 @staticmethod
669 def _check_rel_space(bound: ifcos.entity_instance):
670 """Check existence of related space.
672 And has the correctness of class type.
674 Args:
675 bound: Space boundary IFC instance
677 Returns:
678 True: if check succeeds
679 False: if check fails
680 """
681 return any(
682 [bound.RelatingSpace.is_a('IfcSpace') or
683 bound.RelatingSpace.is_a('IfcExternalSpatialElement')])
685 @staticmethod
686 def _check_rel_building_elem(bound: ifcos.entity_instance):
687 """Check existence of related building element.
689 And the correctness of class type.
691 Args:
692 bound: Space boundary IFC instance
694 Returns:
695 True: if check succeeds
696 False: if check fails
697 """
698 if bound.RelatedBuildingElement is not None:
699 return bound.RelatedBuildingElement.is_a('IfcElement')
701 @staticmethod
702 def _check_conn_geom(bound: ifcos.entity_instance):
703 """Check that the space boundary has a connection geometry.
705 And the correctness of class type.
707 Args:
708 bound: Space boundary IFC instance
710 Returns:
711 True: if check succeeds
712 False: if check fails
713 """
714 if bound.ConnectionGeometry is not None:
715 return bound.ConnectionGeometry.is_a('IfcConnectionGeometry')
717 @staticmethod
718 def _check_on_relating_elem(bound: ifcos.entity_instance):
719 """Check geometric information.
721 Check that the surface on relating element of a space boundary has the
722 geometric information and the correctness of class type.
724 Args:
725 bound: Space boundary IFC instance
727 Returns:
728 True: if check succeeds
729 False: if check fails
731 """
732 if bound.ConnectionGeometry is not None:
733 return bound.ConnectionGeometry.SurfaceOnRelatingElement.is_a(
734 'IfcCurveBoundedPlane')
736 @staticmethod
737 def _check_on_related_elem(bound: ifcos.entity_instance):
738 """Check absence of geometric information.
740 Check that the surface on related element of a space boundary has no
741 geometric information and the correctness of class type.
743 Args:
744 bound: Space boundary IFC instance
746 Returns:
747 True: if check succeeds
748 False: if check fails
749 """
750 if bound.ConnectionGeometry is not None:
751 return (bound.ConnectionGeometry.SurfaceOnRelatedElement is None or
752 bound.ConnectionGeometry.SurfaceOnRelatedElement.is_a(
753 'IfcCurveBoundedPlane'))
755 @staticmethod
756 def _check_basis_surface(bound: ifcos.entity_instance):
757 """Check representation by an IFC Place.
759 Check that the surface on relating element of a space boundary is
760 represented by an IFC Place and the correctness of class type.
762 Args:
763 bound: Space boundary IFC instance
765 Returns:
766 True: if check succeeds
767 False: if check fails
768 """
769 if bound.ConnectionGeometry is not None:
770 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \
771 BasisSurface.is_a('IfcPlane')
773 @staticmethod
774 def _check_inner_boundaries(bound: ifcos.entity_instance):
775 """Check absence of the surface and their structure.
777 Check if the surface on relating element of a space boundary inner
778 boundaries don't exists or are composite curves.
779 Args:
780 bound: Space boundary IFC instance
782 Returns:
783 True: if check succeeds
784 False: if check fails
785 """
786 if bound.ConnectionGeometry is not None:
787 return (bound.ConnectionGeometry.SurfaceOnRelatingElement.
788 InnerBoundaries is None) or \
789 (i.is_a('IfcCompositeCurve') for i in bound.
790 ConnectionGeometry.SurfaceOnRelatingElement.
791 InnerBoundaries)
793 @staticmethod
794 def _check_outer_boundary_composite(bound: ifcos.entity_instance):
795 """Check if the surface are composite curves.
797 Check if the surface on relating element of a space boundary outer
798 boundaries are composite curves.
800 Args:
801 bound: Space boundary IFC instance
803 Returns:
804 True: if check succeeds
805 False: if check fails
806 """
807 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \
808 OuterBoundary.is_a('IfcCompositeCurve')
810 @staticmethod
811 def _check_segments(bound: ifcos.entity_instance):
812 """Check if the surface are poly-line.
814 Check if the surface on relating element of a space boundary outer
815 boundaries segments are poly-line.
817 Args:
818 bound: Space boundary IFC instance
820 Returns:
821 True: if check succeeds
822 False: if check fails
823 """
824 return (s.is_a('IfcCompositeCurveSegment') for s in
825 bound.ConnectionGeometry.SurfaceOnRelatingElement.
826 OuterBoundary.Segments)
828 @staticmethod
829 def _check_coords(points: ifcos.entity_instance):
830 """Check coordinates of a group of points (class and length).
832 Args:
833 points: Points IFC instance
835 Returns:
836 True: if check succeeds
837 False: if check fails
838 """
839 return points.is_a('IfcCartesianPoint') and 1 <= len(
840 points.Coordinates) <= 4
842 @staticmethod
843 def _check_dir_ratios(dir_ratios: ifcos.entity_instance):
844 """Check length of direction ratios.
846 Args:
847 dir_ratios: direction ratios IFC instance
849 Returns:
850 True: if check succeeds
851 False: if check fails
852 """
853 return 2 <= len(dir_ratios.DirectionRatios) <= 3
855 @classmethod
856 def _check_poly_points_coord(cls, polyline: ifcos.entity_instance):
857 """Check if a poly-line has the correct coordinates.
859 Args:
860 polyline: Poly-line IFC instance
862 Returns:
863 True: if check succeeds
864 False: if check fails
865 """
866 return all(cls._check_coords(p) for p in polyline.Points)
868 @classmethod
869 def _check_segments_poly_coord(cls, bound: ifcos.entity_instance):
870 """Check segments coordinates.
872 Check segments coordinates of an outer boundary of a surface on
873 relating element.
875 Args:
876 bound: Space boundary IFC instance
878 Returns:
879 True: if check succeeds
880 False: if check fails
881 """
882 return all(cls._check_poly_points_coord(s.ParentCurve)
883 for s in
884 bound.ConnectionGeometry.SurfaceOnRelatingElement.
885 OuterBoundary.Segments)
887 @staticmethod
888 def _check_poly_points(polyline: ifcos.entity_instance):
889 """Check if a polyline has the correct class.
891 Args:
892 polyline: Polyline IFC instance
894 Returns:
895 True: if check succeeds
896 False: if check fails
897 """
898 return polyline.is_a('IfcPolyline')
900 @classmethod
901 def _check_outer_boundary_poly(cls, bound: ifcos.entity_instance):
902 """Check points of outer boundary of a surface on relating element.
904 Args:
905 bound: Space boundary IFC instance
907 Returns:
908 True: if check succeeds
909 False: if check fails
910 """
911 return cls._check_poly_points(
912 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary)
914 @staticmethod
915 def _check_outer_boundary_poly_coord(bound: ifcos.entity_instance):
916 """Check outer boundary of a surface on relating element.
918 Args:
919 bound: Space boundary IFC instance
921 Returns:
922 True: if check succeeds
923 False: if check fails
924 """
925 return all(
926 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary)
928 @staticmethod
929 def _check_plane_position(bound: ifcos.entity_instance):
930 """
931 Check class of plane position of space boundary.
933 Args:
934 bound: Space boundary IFC instance
936 Returns:
937 True: if check succeeds
938 False: if check fails
939 """
940 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
941 Position.is_a('IfcAxis2Placement3D')
943 @staticmethod
944 def _check_location(bound: ifcos.entity_instance):
945 """
946 Check that location of a space boundary is an IfcCartesianPoint.
948 Args:
949 bound: Space boundary IFC instance
951 Returns:
952 True: if check succeeds
953 False: if check fails
954 """
955 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
956 Position.Location.is_a('IfcCartesianPoint')
958 @staticmethod
959 def _check_axis(bound: ifcos.entity_instance):
960 """Check that axis of space boundary is an IfcDirection.
962 Args:
963 bound: Space boundary IFC instance
965 Returns:
966 True: if check succeeds
967 False: if check fails
968 """
969 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
970 Position.Axis.is_a('IfcDirection')
972 @staticmethod
973 def _check_refdirection(bound: ifcos.entity_instance):
974 """Check that reference direction of space boundary is an IfcDirection.
976 Args:
977 bound: Space boundary IFC instance
979 Returns:
980 True: if check succeeds
981 False: if check fails
982 """
983 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \
984 Position.RefDirection.is_a('IfcDirection')
986 @classmethod
987 def _check_location_coord(cls, bound: ifcos.entity_instance):
988 """Check the correctness of related coordinates.
990 Check if space boundary surface on relating element coordinates are
991 correct.
993 Args:
994 bound: Space boundary IFC instance
996 Returns:
997 True: if check succeeds
998 False: if check fails
1000 """
1001 return cls._check_coords(bound.ConnectionGeometry.
1002 SurfaceOnRelatingElement.BasisSurface.
1003 Position.Location)
1005 @classmethod
1006 def _check_axis_dir_ratios(cls, bound: ifcos.entity_instance):
1007 """Check correctness of space boundary surface.
1009 Check if space boundary surface on relating element axis are correct.
1011 Args:
1012 bound: Space boundary IFC instance
1014 Returns:
1015 True: if check succeeds
1016 False: if check fails
1017 """
1018 return cls._check_dir_ratios(
1019 bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface.
1020 Position.Axis)
1022 @classmethod
1023 def _check_refdirection_dir_ratios(cls, bound: ifcos.entity_instance):
1024 """Check correctness of space boundary surface.
1026 Check if space boundary surface on relating element reference direction
1027 are correct.
1029 Args:
1030 bound: Space boundary IFC instance
1032 Returns:
1033 True: if check succeeds
1034 False: if check fails
1035 """
1036 return cls._check_dir_ratios(
1037 bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface.
1038 Position.RefDirection)
1040 @staticmethod
1041 def _check_inst_sb(inst: ifcos.entity_instance):
1042 """Check association of an instance.
1044 Check that an instance has associated space boundaries (space or
1045 building element).
1047 Args:
1048 inst: IFC instance
1050 Returns:
1051 True: if check succeeds
1052 False: if check fails
1053 """
1054 blacklist = ['IfcBuilding', 'IfcSite', 'IfcBuildingStorey',
1055 'IfcMaterial', 'IfcMaterialLayer', 'IfcMaterialLayerSet']
1056 if inst.is_a() in blacklist:
1057 return True
1058 elif inst.is_a('IfcSpace') or inst.is_a('IfcExternalSpatialElement'):
1059 return len(inst.BoundedBy) > 0
1060 else:
1061 if len(inst.ProvidesBoundaries) > 0:
1062 return True
1063 decompose = []
1064 if hasattr(inst, 'Decomposes') and len(inst.Decomposes):
1065 decompose = [decomp.RelatingObject for decomp in
1066 inst.Decomposes]
1067 elif hasattr(inst, 'IsDecomposedBy') and len(inst.IsDecomposedBy):
1068 decompose = []
1069 for decomp in inst.IsDecomposedBy:
1070 for inst_ifc in decomp.RelatedObjects:
1071 decompose.append(inst_ifc)
1072 for inst_decomp in decompose:
1073 if len(inst_decomp.ProvidesBoundaries):
1074 return True
1075 return False
1077 @staticmethod
1078 def _check_inst_materials(inst: ifcos.entity_instance):
1079 """Check that an instance has associated materials.
1081 Args:
1082 inst: IFC instance
1084 Returns:
1085 True: if check succeeds
1086 False: if check fails
1087 """
1088 blacklist = [
1089 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace',
1090 'IfcExternalSpatialElement']
1091 if not (inst.is_a() in blacklist):
1092 return len(get_layers_ifc(inst)) > 0
1093 return True
1095 def _check_inst_properties(self, inst: ifcos.entity_instance):
1096 """Check existence of necessary property sets and properties.
1098 Check that an instance has the property sets and properties
1099 necessaries to the plugin.
1100 Args:
1101 inst: IFC instance
1103 Returns:
1104 True: if check succeeds
1105 False: if check fails
1106 """
1107 inst_prop2check = self.ps_summary.get(inst.is_a(), {})
1108 inst_prop = get_property_sets(inst, self.ifc_units)
1109 inst_prop_errors = []
1110 for prop2check, ps2check in inst_prop2check.items():
1111 ps = inst_prop.get(ps2check[0], None)
1112 if ps:
1113 if not ps.get(ps2check[1], None):
1114 inst_prop_errors.append(
1115 prop2check+' - '+', '.join(ps2check))
1116 else:
1117 inst_prop_errors.append(prop2check+' - '+', '.join(ps2check))
1118 if inst_prop_errors:
1119 key = inst.GlobalId + ' ' + inst.is_a()
1120 self.error_summary_prop.update({key: inst_prop_errors})
1121 return False
1122 return True
1124 @staticmethod
1125 def _check_inst_contained_in_structure(inst: ifcos.entity_instance):
1126 """Check that an instance is contained in an structure.
1128 Args:
1129 inst: IFC instance
1131 Returns:
1132 True: if check succeeds
1133 False: if check fails
1134 """
1135 blacklist = [
1136 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace',
1137 'IfcExternalSpatialElement', 'IfcMaterial', 'IfcMaterialLayer',
1138 'IfcMaterialLayerSet'
1139 ]
1140 if not (inst.is_a() in blacklist):
1141 return len(inst.ContainedInStructure) > 0
1142 if hasattr(inst, 'Decomposes'):
1143 return len(inst.Decomposes) > 0
1144 else:
1145 return True
1147 @staticmethod
1148 def _check_inst_representation(inst: ifcos.entity_instance):
1149 """
1150 Check that an instance has a correct geometric representation.
1152 Args:
1153 inst: IFC instance
1155 Returns:
1156 True: if check succeeds
1157 False: if check fails
1158 """
1159 blacklist = [
1160 'IfcBuilding', 'IfcBuildingStorey', 'IfcMaterial',
1161 'IfcMaterialLayer', 'IfcMaterialLayerSet'
1162 ]
1163 if not (inst.is_a() in blacklist):
1164 return inst.Representation is not None
1165 return True
1167 def validate_sub_inst(self, bound: ifcos.entity_instance) -> list:
1168 """Validate space boundary.
1170 Validation function for a space boundary that compiles all validation
1171 functions.
1173 Args:
1174 bound: ifc space boundary entity
1176 Returns:
1177 error: list of errors found in the ifc space boundaries
1178 """
1179 error = []
1180 # print(bound)
1181 self.apply_validation_function(self._check_rel_space(bound),
1182 'RelatingSpace - '
1183 'The space boundary does not have a '
1184 'relating space associated', error)
1185 self.apply_validation_function(self._check_rel_building_elem(bound),
1186 'RelatedBuildingElement - '
1187 'The space boundary does not have a '
1188 'related building element associated',
1189 error)
1190 self.apply_validation_function(self._check_conn_geom(bound),
1191 'ConnectionGeometry - '
1192 'The space boundary does not have a '
1193 'connection geometry', error)
1194 self.apply_validation_function(self._check_on_relating_elem(bound),
1195 'SurfaceOnRelatingElement - '
1196 'The space boundary does not have a '
1197 'surface on the relating element',
1198 error)
1199 self.apply_validation_function(self._check_on_related_elem(bound),
1200 'SurfaceOnRelatedElement - '
1201 'The space boundary does not have a '
1202 'surface on the related element', error)
1203 self.apply_validation_function(self._check_basis_surface(bound),
1204 'BasisSurface - '
1205 'The space boundary surface on '
1206 'relating element geometry is missing',
1207 error)
1208 self.apply_validation_function(self._check_inner_boundaries(bound),
1209 'InnerBoundaries - '
1210 'The space boundary surface on '
1211 'relating element inner boundaries are '
1212 'missing', error)
1213 if bound.ConnectionGeometry is not None:
1214 if hasattr(
1215 bound.ConnectionGeometry.SurfaceOnRelatingElement.
1216 OuterBoundary, 'Segments'):
1217 self.apply_validation_function(
1218 self._check_outer_boundary_composite(bound),
1219 'OuterBoundary - '
1220 'The space boundary surface on relating element outer '
1221 'boundary is missing', error)
1222 self.apply_validation_function(self._check_segments(bound),
1223 'OuterBoundary Segments - '
1224 'The space boundary surface on '
1225 'relating element outer '
1226 'boundary '
1227 'geometry is missing', error)
1228 self.apply_validation_function(
1229 self._check_segments_poly_coord(bound),
1230 'OuterBoundary Coordinates - '
1231 'The space boundary surface on relating element outer '
1232 'boundary coordinates are missing', error)
1233 else:
1234 self.apply_validation_function(
1235 self._check_outer_boundary_poly(bound),
1236 'OuterBoundary - '
1237 'The space boundary surface on relating element outer '
1238 'boundary is missing', error)
1239 self.apply_validation_function(
1240 self._check_outer_boundary_poly_coord(bound),
1241 'OuterBoundary Coordinates - '
1242 'The space boundary surface on relating element outer '
1243 'boundary coordinates are missing', error)
1244 self.apply_validation_function(self._check_plane_position(bound),
1245 'Position - '
1246 'The space boundary surface on'
1247 'relating '
1248 'element plane position is missing',
1249 error)
1250 self.apply_validation_function(self._check_location(bound),
1251 'Location - '
1252 'The space boundary surface on '
1253 'relating element location is '
1254 'missing', error)
1255 self.apply_validation_function(self._check_axis(bound),
1256 'Axis - '
1257 'The space boundary surface on'
1258 'relating '
1259 'element axis are missing',
1260 error)
1261 self.apply_validation_function(self._check_refdirection(bound),
1262 'RefDirection - '
1263 'The space boundary surface on '
1264 'relating '
1265 'element reference direction is '
1266 'missing', error)
1267 self.apply_validation_function(self._check_location_coord(bound),
1268 'LocationCoordinates - '
1269 'The space boundary surface on'
1270 'relating '
1271 'element location coordinates are '
1272 'missing', error)
1273 self.apply_validation_function(self._check_axis_dir_ratios(bound),
1274 'AxisDirectionRatios - '
1275 'The space boundary surface on '
1276 'relating '
1277 'element axis direction ratios are '
1278 'missing', error)
1279 self.apply_validation_function(
1280 self._check_refdirection_dir_ratios(bound),
1281 'RefDirectionDirectionRatios - '
1282 'The space boundary surface on relating element position '
1283 'reference direction is missing', error)
1284 return error
1286 def validate_elements(self, inst: ifcos.entity_instance) -> list:
1287 """Validate elements.
1289 Validation function for an instance that compiles all instance
1290 validation functions.
1291 Args:
1292 inst:IFC instance being checked
1294 Returns:
1295 error: list of elements error
1297 """
1298 error = []
1299 self.apply_validation_function(self._check_inst_sb(inst),
1300 'SpaceBoundaries - '
1301 'The instance space boundaries are '
1302 'missing', error)
1303 self.apply_validation_function(self._check_inst_materials(inst),
1304 'MaterialLayers - '
1305 'The instance materials are missing',
1306 error)
1307 self.apply_validation_function(self._check_inst_properties(inst),
1308 'Missing Property_Sets - '
1309 'One or more instance\'s necessary '
1310 'property sets are missing', error)
1311 self.apply_validation_function(
1312 self._check_inst_contained_in_structure(inst),
1313 'ContainedInStructure - '
1314 'The instance is not contained in any '
1315 'structure', error)
1316 self.apply_validation_function(self._check_inst_representation(inst),
1317 'Representation - '
1318 'The instance has no geometric '
1319 'representation', error)
1320 return error
1323class CheckLogicHVAC(CheckLogicBase):
1324 """Provides additional logic for ifc files checking regarding HVAC."""
1326 @staticmethod
1327 def _check_assignments(inst: ifcos.entity_instance) -> bool:
1328 """Check that the inst (also spec. port) has at least one assignment.
1330 Args:
1331 port: port ifc entity
1333 Returns:
1334 True: if check succeeds
1335 False: if check fails
1336 """
1337 return any(assign.is_a('IfcRelAssignsToGroup') for assign in
1338 inst.HasAssignments)
1340 @staticmethod
1341 def _check_connection(port: ifcos.entity_instance) -> bool:
1342 """Check that the port is: "connected_to" or "connected_from".
1344 Args:
1345 port: port ifc entity
1347 Returns:
1348 True: if check succeeds
1349 False: if check fails
1350 """
1351 return len(port.ConnectedTo) > 0 or len(port.ConnectedFrom) > 0
1353 @staticmethod
1354 def _check_contained_in(port: ifcos.entity_instance) -> bool:
1355 """
1356 Check that the port is "contained_in".
1358 Args:
1359 port: port ifc entity
1361 Returns:
1362 True: if check succeeds
1363 False: if check fails
1364 """
1365 return len(port.ContainedIn) > 0
1367 # elements check
1368 @staticmethod
1369 def _check_inst_ports(inst: ifcos.entity_instance) -> bool:
1370 """Check that an instance has associated ports.
1372 Args:
1373 inst: IFC instance
1375 Returns:
1376 True: if check succeeds
1377 False: if check fails
1378 """
1379 ports = get_ports(inst)
1380 if ports:
1381 return True
1382 else:
1383 return False
1385 @staticmethod
1386 def _check_contained_in_structure(inst: ifcos.entity_instance) -> bool:
1387 """Check that an instance is contained in an structure.
1389 Args:
1390 inst: IFC instance
1392 Returns:
1393 True: if check succeeds
1394 False: if check fails
1395 """
1396 if hasattr(inst, 'ContainedInStructure'):
1397 return len(inst.ContainedInStructure) > 0
1398 else:
1399 return False
1401 def _check_inst_properties(self, inst: ifcos.entity_instance):
1402 """Check necessaries property sets and properties.
1404 Check that an instance has the property sets and properties
1405 necessaries to the plugin.
1406 Args:
1407 inst: IFC instance
1409 Returns:
1410 True: if check succeeds
1411 False: if check fails
1412 """
1413 inst_prop2check = self.ps_summary.get(inst.is_a(), {})
1414 inst_prop = get_property_sets(inst, self.ifc_units)
1415 inst_prop_errors = []
1416 for prop2check, ps2check in inst_prop2check.items():
1417 ps = inst_prop.get(ps2check[0], None)
1418 if ps:
1419 if not ps.get(ps2check[1], None):
1420 inst_prop_errors.append(
1421 prop2check+' - '+', '.join(ps2check))
1422 else:
1423 inst_prop_errors.append(prop2check+' - '+', '.join(ps2check))
1424 if inst_prop_errors:
1425 key = inst.GlobalId + ' ' + inst.is_a()
1426 self.error_summary_prop.update({key: inst_prop_errors})
1427 return False
1428 return True
1430 @staticmethod
1431 def _check_inst_representation(inst: ifcos.entity_instance):
1432 """Check that an instance has a correct geometric representation.
1434 Args:
1435 inst: IFC instance
1437 Returns:
1438 True: if check succeeds
1439 False: if check fails
1440 """
1441 if hasattr(inst, 'Representation'):
1442 return inst.Representation is not None
1443 else:
1444 return False
1446 def validate_sub_inst(self, port: ifcos.entity_instance) -> list:
1447 """Run validation functions for a port.
1449 Args:
1450 port: IFC port entity
1452 Returns:
1453 error: list of errors found in the IFC port
1455 """
1456 error = []
1457 self.apply_validation_function(self._check_assignments(port),
1458 'Assignments - '
1459 'The port assignments are missing',
1460 error)
1461 self.apply_validation_function(self._check_connection(port),
1462 'Connections - '
1463 'The port has no connections', error)
1464 self.apply_validation_function(self._check_contained_in(port),
1465 'ContainedIn - '
1466 'The port is not contained in', error)
1467 return error
1469 def validate_elements(self, inst: ifcos.entity_instance) -> list:
1470 """Validate elements (carrier function).
1472 Validation function for an instance that compiles all instance
1473 validation functions.
1475 Args:
1476 inst: IFC instance being checked
1478 Returns:
1479 error: list of elements error
1481 """
1482 error = []
1484 self.apply_validation_function(self._check_inst_ports(inst),
1485 'Ports - '
1486 'The instance ports are missing', error)
1487 self.apply_validation_function(
1488 self._check_contained_in_structure(inst),
1489 'ContainedInStructure - '
1490 'The instance is not contained in any '
1491 'structure', error)
1492 self.apply_validation_function(self._check_inst_properties(inst),
1493 'Missing Property_Sets - '
1494 'One or more instance\'s necessary '
1495 'property sets are missing', error)
1496 self.apply_validation_function(self._check_inst_representation(inst),
1497 'Representation - '
1498 'The instance has no geometric '
1499 'representation', error)
1500 self.apply_validation_function(self._check_assignments(inst),
1501 'Assignments - '
1502 'The instance assignments are missing',
1503 error)
1504 return error
1507if __name__ == '__main__':
1508 pass