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

1from __future__ import annotations 

2 

3import types 

4import inspect 

5import json 

6import logging 

7import os 

8import warnings 

9from typing import Dict, Callable 

10 

11from ifcopenshell import file, entity_instance 

12from mako.lookup import TemplateLookup 

13from mako.template import Template 

14 

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 

23 

24 

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',) 

31 

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 

44 

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. 

49 

50 Args: 

51 ifc_files: bim2sim IfcFileClass holding the ifcopenshell ifc 

52 instance 

53 

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) 

108 

109 def check_critical_errors(self, ifc: file, id_list: list): 

110 """ 

111 Checks for critical errors in the IFC file. 

112 

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) 

121 

122 @staticmethod 

123 def check_ifc_version(ifc: file): 

124 """ 

125 Checks the IFC version. 

126 

127 Only IFC4 files are valid for bim2sim. 

128 

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.") 

139 

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) 

147 

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 = {} 

157 

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 

168 

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) 

177 

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 

192 

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 

197 

198 Args: 

199 ifc: IFC file translated with ifcopenshell 

200 

201 Returns: 

202 ifc_elements: list of IFC instance (Products) 

203 

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 

210 

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 

220 

221 Returns: 

222 summary: summarized dictionary of errors, where the key is the 

223 GUID + the ifc_type 

224 

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 

236 

237 def validate_sub_inst(self, sub_inst: list) -> list: 

238 raise NotImplementedError 

239 

240 def validate_elements(self, inst: list) -> list: 

241 raise NotImplementedError 

242 

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. 

248 

249 Args: 

250 fct: validation function to be applied 

251 err_name: string that define the error 

252 error: list of errors 

253 

254 """ 

255 if not fct: 

256 error.append(err_name) 

257 

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. 

262 

263 Args:ps 

264 base_name: str of file base name for reports 

265 

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") 

277 

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 

288 

289 Returns: 

290 categorized_dict: dictionary containing all errors categorized 

291 

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 

309 

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 

315 

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 

330 

331 @staticmethod 

332 def check_critical_uniqueness(id_list: list): 

333 """ 

334 Checks if all GlobalIds are unique. 

335 

336 Only files containing unique GUIDs are valid for bim2sim. 

337 

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.") 

355 

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. 

360 

361 Args: 

362 inst: IFC instance 

363 

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 

384 

385 @staticmethod 

386 def _check_inst_representation(inst: entity_instance): 

387 """ 

388 Check that an instance has a correct geometric representation. 

389 

390 Args: 

391 inst: IFC instance 

392 

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 

401 

402 def get_html_templates(self): 

403 """ 

404 Gets all stored html templates that will be used to export the errors 

405 summaries 

406 

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 

424 

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 

428 

429 Args: 

430 base_name: str of file base name for reports 

431 domain: IFCDomain of the checked IFC 

432 """ 

433 

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']} 

439 

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() 

469 

470 

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 """ 

476 

477 def __init__(self, playground: Playground): 

478 super().__init__(playground) 

479 self.sub_inst_cls = 'IfcDistributionPort' 

480 self.plugin = hvac 

481 

482 def validate_sub_inst(self, port: entity_instance) -> list: 

483 """ 

484 Validation function for a port that compiles all validation functions. 

485 

486 Args: 

487 port: IFC port entity 

488 

489 Returns: 

490 error: list of errors found in the IFC port 

491 

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) 

510 

511 return error 

512 

513 def validate_elements(self, inst: entity_instance) -> list: 

514 """ 

515 Validation function for an instance that compiles all instance validation functions. 

516 

517 Args: 

518 inst: IFC instance being checked 

519 

520 Returns: 

521 error: list of elements error 

522 

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) 

546 

547 return error 

548 

549 @staticmethod 

550 def _check_flow_direction(port: entity_instance) -> bool: 

551 """ 

552 Check that the port has a defined flow direction. 

553 

554 Args: 

555 port: port IFC entity 

556 

557 Returns: 

558 True if check succeeds, False otherwise 

559 """ 

560 return port.FlowDirection in ['SOURCE', 'SINK', 'SINKANDSOURCE', 

561 'SOURCEANDSINK'] 

562 

563 @staticmethod 

564 def _check_assignments(port: entity_instance) -> bool: 

565 """ 

566 Check that the port has at least one assignment. 

567 

568 Args: 

569 port: port ifc entity 

570 

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) 

577 

578 @staticmethod 

579 def _check_connection(port: entity_instance) -> bool: 

580 """ 

581 Check that the port is: "connected_to" or "connected_from". 

582 

583 Args: 

584 port: port ifc entity 

585 

586 Returns: 

587 True: if check succeeds 

588 False: if check fails 

589 """ 

590 return len(port.ConnectedTo) > 0 or len(port.ConnectedFrom) > 0 

591 

592 @staticmethod 

593 def _check_contained_in(port: entity_instance) -> bool: 

594 """ 

595 Check that the port is "contained_in". 

596 

597 Args: 

598 port: port ifc entity 

599 

600 Returns: 

601 True: if check succeeds 

602 False: if check fails 

603 """ 

604 return len(port.ContainedIn) > 0 

605 

606 # elements check 

607 @staticmethod 

608 def _check_inst_ports(inst: entity_instance) -> bool: 

609 """ 

610 Check that an instance has associated ports. 

611 

612 Args: 

613 inst: IFC instance 

614 

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 

624 

625 @staticmethod 

626 def _check_contained_in_structure(inst: entity_instance) -> bool: 

627 """ 

628 Check that an instance is contained in an structure. 

629 

630 Args: 

631 inst: IFC instance 

632 

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 

641 

642 

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 """ 

649 

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 

655 

656 def check_critical_errors(self, ifc: file, id_list: list): 

657 """ 

658 Checks for critical errors in the IFC file. 

659 

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() 

670 

671 def check_sub_inst_exist(self): 

672 """ 

673 Checks for the existence of IfcRelSpaceBoundaries. 

674 

675 Only files containing elements of type 'IfcRelSpaceBoundary' are 

676 valid for bim2sim. 

677 

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.") 

687 

688 def check_rel_space_exist(self): 

689 """ 

690 Checks for the existence of RelatedSpace attribute of 

691 IfcRelSpaceBoundaries. 

692 

693 Only IfcRelSpaceBoundaries with an IfcSpace or 

694 IfcExternalSpatialElement are valid for bim2sim. 

695 

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.") 

712 

713 def validate_sub_inst(self, bound: entity_instance) -> list: 

714 """ 

715 Validation function for a space boundary that compiles all validation 

716 functions. 

717 

718 Args: 

719 bound: ifc space boundary entity 

720 

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) 

813 

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) 

848 

849 return error 

850 

851 def validate_elements(self, inst: entity_instance) -> list: 

852 """ 

853 Validation function for an instance that compiles all instance 

854 validation functions. 

855 

856 Args: 

857 inst:IFC instance being checked 

858 

859 Returns: 

860 error: list of elements error 

861 

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 

889 

890 @staticmethod 

891 def _check_level(bound: entity_instance): 

892 """ 

893 Check that the space boundary is of the second level type 

894 

895 Args: 

896 bound: Space boundary IFC instance 

897 

898 Returns: 

899 True: if check succeeds 

900 False: if check fails 

901 """ 

902 return bound.Name == "2ndLevel" 

903 

904 @staticmethod 

905 def _check_description(bound: entity_instance): 

906 """ 

907 Check that the space boundary description is 2a or 2b 

908 

909 Args: 

910 bound: Space boundary IFC instance 

911 

912 Returns: 

913 True: if check succeeds 

914 False: if check fails 

915 """ 

916 return bound.Description in {'2a', '2b'} 

917 

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. 

923 

924 Args: 

925 bound: Space boundary IFC instance 

926 

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')]) 

934 

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. 

940 

941 Args: 

942 bound: Space boundary IFC instance 

943 

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') 

950 

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. 

956 

957 Args: 

958 bound: Space boundary IFC instance 

959 

960 Returns: 

961 True: if check succeeds 

962 False: if check fails 

963 """ 

964 return bound.ConnectionGeometry.is_a('IfcConnectionGeometry') 

965 

966 @staticmethod 

967 def _check_phys_virt_bound(bound: entity_instance): 

968 """ 

969 Check that the space boundary is virtual or physical. 

970 

971 Args: 

972 bound: Space boundary IFC instance 

973 

974 Returns: 

975 True: if check succeeds 

976 False: if check fails 

977 """ 

978 return bound.PhysicalOrVirtualBoundary.upper() in \ 

979 {'PHYSICAL', 'VIRTUAL', 'NOTDEFINED'} 

980 

981 @staticmethod 

982 def _check_int_ext_bound(bound: entity_instance): 

983 """ 

984 Check that the space boundary is internal or external. 

985 

986 Args: 

987 bound: Space boundary IFC instance 

988 

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 } 

999 

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. 

1005 

1006 Args: 

1007 bound: Space boundary IFC instance 

1008 

1009 Returns: 

1010 True: if check succeeds 

1011 False: if check fails 

1012 """ 

1013 return bound.ConnectionGeometry.SurfaceOnRelatingElement.is_a( 

1014 'IfcCurveBoundedPlane') 

1015 

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. 

1021 

1022 Args: 

1023 bound: Space boundary IFC instance 

1024 

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')) 

1032 

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. 

1038 

1039 Args: 

1040 bound: Space boundary IFC instance 

1041 

1042 Returns: 

1043 True: if check succeeds 

1044 False: if check fails 

1045 """ 

1046 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \ 

1047 BasisSurface.is_a('IfcPlane') 

1048 

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. 

1054 

1055 Args: 

1056 bound: Space boundary IFC instance 

1057 

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) 

1066 

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. 

1072 

1073 Args: 

1074 bound: Space boundary IFC instance 

1075 

1076 Returns: 

1077 True: if check succeeds 

1078 False: if check fails 

1079 """ 

1080 return bound.ConnectionGeometry.SurfaceOnRelatingElement. \ 

1081 OuterBoundary.is_a('IfcCompositeCurve') 

1082 

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. 

1088 

1089 Args: 

1090 bound: Space boundary IFC instance 

1091 

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) 

1099 

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. 

1104 

1105 Args: 

1106 bound: Space boundary IFC instance 

1107 

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) 

1116 

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. 

1122 

1123 Args: 

1124 bound: Space boundary IFC instance 

1125 

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) 

1134 

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. 

1139 

1140 Args: 

1141 bound: Space boundary IFC instance 

1142 

1143 Returns: 

1144 True: if check succeeds 

1145 False: if check fails 

1146 """ 

1147 return cls._check_poly_points( 

1148 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary) 

1149 

1150 @staticmethod 

1151 def _check_outer_boundary_poly_coord(bound: entity_instance): 

1152 """ 

1153 Check outer boundary of a surface on relating element. 

1154 

1155 Args: 

1156 bound: Space boundary IFC instance 

1157 

1158 Returns: 

1159 True: if check succeeds 

1160 False: if check fails 

1161 """ 

1162 return all( 

1163 bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary) 

1164 

1165 @staticmethod 

1166 def _check_plane_position(bound: entity_instance): 

1167 """ 

1168 Check class of plane position of space boundary. 

1169 

1170 Args: 

1171 bound: Space boundary IFC instance 

1172 

1173 Returns: 

1174 True: if check succeeds 

1175 False: if check fails 

1176 """ 

1177 return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \ 

1178 Position.is_a('IfcAxis2Placement3D') 

1179 

1180 @staticmethod 

1181 def _check_location(bound: entity_instance): 

1182 """ 

1183 Check that location of a space boundary is an IfcCartesianPoint. 

1184 

1185 Args: 

1186 bound: Space boundary IFC instance 

1187 

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') 

1194 

1195 @staticmethod 

1196 def _check_axis(bound: entity_instance): 

1197 """ 

1198 Check that axis of space boundary is an IfcDirection. 

1199 

1200 Args: 

1201 bound: Space boundary IFC instance 

1202 

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') 

1209 

1210 @staticmethod 

1211 def _check_refdirection(bound: entity_instance): 

1212 """ 

1213 Check that reference direction of space boundary is an IfcDirection. 

1214 

1215 Args: 

1216 bound: Space boundary IFC instance 

1217 

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') 

1224 

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. 

1230 

1231 Args: 

1232 bound: Space boundary IFC instance 

1233 

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) 

1241 

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. 

1246 

1247 Args: 

1248 bound: Space boundary IFC instance 

1249 

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) 

1257 

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. 

1263 

1264 Args: 

1265 bound: Space boundary IFC instance 

1266 

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) 

1274 

1275 @staticmethod 

1276 def _check_poly_points(polyline: entity_instance): 

1277 """ 

1278 Check if a polyline has the correct class. 

1279 

1280 Args: 

1281 polyline: Polyline IFC instance 

1282 

1283 Returns: 

1284 True: if check succeeds 

1285 False: if check fails 

1286 """ 

1287 return polyline.is_a('IfcPolyline') 

1288 

1289 @staticmethod 

1290 def _check_coords(points: entity_instance): 

1291 """ 

1292 Check coordinates of a group of points (class and length). 

1293 

1294 Args: 

1295 points: Points IFC instance 

1296 

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 

1303 

1304 @staticmethod 

1305 def _check_dir_ratios(dir_ratios: entity_instance): 

1306 """ 

1307 Check length of direction ratios. 

1308 

1309 Args: 

1310 dir_ratios: direction ratios IFC instance 

1311 

1312 Returns: 

1313 True: if check succeeds 

1314 False: if check fails 

1315 """ 

1316 return 2 <= len(dir_ratios.DirectionRatios) <= 3 

1317 

1318 @classmethod 

1319 def _check_poly_points_coord(cls, polyline: entity_instance): 

1320 """ 

1321 Check if a polyline has the correct coordinates. 

1322 

1323 Args: 

1324 polyline: Polyline IFC instance 

1325 

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) 

1331 

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). 

1337 

1338 Args: 

1339 inst: IFC instance 

1340 

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 

1367 

1368 @staticmethod 

1369 def _check_inst_materials(inst: entity_instance): 

1370 """ 

1371 Check that an instance has associated materials. 

1372 

1373 Args: 

1374 inst: IFC instance 

1375 

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 

1386 

1387 @staticmethod 

1388 def _check_inst_contained_in_structure(inst: entity_instance): 

1389 """ 

1390 Check that an instance is contained in an structure. 

1391 

1392 Args: 

1393 inst: IFC instance 

1394 

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 

1410 

1411 @staticmethod 

1412 def _check_inst_representation(inst: entity_instance): 

1413 """ 

1414 Check that an instance has a correct geometric representation. 

1415 

1416 Args: 

1417 inst: IFC instance 

1418 

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