Coverage for bim2sim/tasks/common/create_elements.py: 60%

263 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

1from __future__ import annotations 

2 

3import logging 

4from typing import Tuple, List, Any, Generator, Dict, Type, Set 

5import copy 

6 

7from bim2sim.elements import bps_elements as bps 

8from bim2sim.elements.base_elements import Factory, ProductBased, Material, Element 

9from bim2sim.elements.mapping import ifc2python 

10from bim2sim.elements.mapping.filter import TypeFilter, TextFilter 

11from bim2sim.kernel import IFCDomainError 

12from bim2sim.kernel.decision import DecisionBunch, ListDecision, Decision 

13from bim2sim.kernel.ifc_file import IfcFileClass 

14from bim2sim.sim_settings import BaseSimSettings 

15from bim2sim.tasks.base import ITask 

16from bim2sim.utilities.common_functions import group_by_levenshtein 

17from bim2sim.utilities.types import LOD 

18from bim2sim.tasks.base import Playground 

19from ifcopenshell import file, entity_instance 

20 

21 

22class CreateElementsOnIfcTypes(ITask): 

23 """Create bim2sim elements based on information of IFC types.""" 

24 

25 reads = ('ifc_files',) 

26 touches = ('elements', '_initial_elements', 'ifc_files') 

27 

28 def __init__(self, playground: Playground): 

29 super().__init__(playground) 

30 self.factory = None 

31 self.source_tools: list = [] 

32 self.layersets_all: list = [] 

33 self.materials_all: list = [] 

34 self.layers_all: list = [] 

35 

36 def run(self, ifc_files: [IfcFileClass]) -> ( 

37 Tuple)[Dict[Any, Element], Dict[Any, Element], List[IfcFileClass]]: 

38 """This task creates the bim2sim elements based on the ifc data. 

39 

40 For each ifc file a factory instance is created. The factory instance 

41 allows the easy creation of bim2sim elements based on ifc elements. 

42 As we might not want to create bim2sim elements for every existing ifc 

43 element, we use the concept of relevant_elements which are taken from 

44 the sim_setting relevant_elements. This way the user can describe which 

45 bim2sim elements are relevant for the respective simulation and only 

46 the fitting ifc elements are taken into account. 

47 During the creation of the bim2sim elements validations are performed, 

48 to make sure that the resulting bim2sim elements hold valid 

49 information. 

50 

51 Args: 

52 ifc_files: list of ifc files in bim2sim structured format 

53 Returns: 

54 elements: bim2sim elements created based on ifc data 

55 ifc_files: list of ifc files in bim2sim structured format 

56 """ 

57 self.logger.info("Creates elements of relevant ifc types") 

58 default_ifc_types = {'IfcBuildingElementProxy', 'IfcUnitaryEquipment'} 

59 # Todo maybe move this into IfcFileClass instead simulation settings 

60 relevant_elements = self.playground.sim_settings.relevant_elements 

61 relevant_ifc_types = self.get_ifc_types(relevant_elements) 

62 relevant_ifc_types.update(default_ifc_types) 

63 

64 elements: dict = {} 

65 for ifc_file in ifc_files: 

66 self.factory = Factory( 

67 relevant_elements, 

68 ifc_file.ifc_units, 

69 ifc_file.domain, 

70 ifc_file.finder) 

71 

72 # Filtering: 

73 # filter returns dict of entities: suggested class and list of unknown 

74 # accept_valids returns created elements and lst of invalids 

75 

76 element_lst: list = [] 

77 entity_best_guess_dict: dict = {} 

78 # filter by type 

79 type_filter = TypeFilter(relevant_ifc_types) 

80 entity_type_dict, unknown_entities = type_filter.run(ifc_file.file) 

81 

82 # create valid elements 

83 valids, invalids = self.create_with_validation(entity_type_dict) 

84 element_lst.extend(valids) 

85 unknown_entities.extend(invalids) 

86 

87 # filter by text 

88 text_filter = TextFilter( 

89 relevant_elements, 

90 ifc_file.ifc_units, 

91 ['Description']) 

92 entity_class_dict, unknown_entities = yield from self.filter_by_text( 

93 text_filter, unknown_entities, ifc_file.ifc_units) 

94 entity_best_guess_dict.update(entity_class_dict) 

95 # TODO why do we run this two times, once without and once with 

96 # force=True 

97 valids, invalids = self.create_with_validation( 

98 entity_class_dict, force=True) 

99 element_lst.extend(valids) 

100 unknown_entities.extend(invalids) 

101 

102 self.logger.info("Found %d relevant elements", len(element_lst)) 

103 self.logger.info("Found %d ifc_entities that could not be " 

104 "identified and therefore not converted into a" 

105 " bim2sim element.", 

106 len(unknown_entities)) 

107 

108 # identification of remaining entities by user 

109 entity_class_dict, unknown_entities = yield from self.set_class_by_user( 

110 unknown_entities, 

111 self.playground.sim_settings, 

112 entity_best_guess_dict) 

113 entity_best_guess_dict.update(entity_class_dict) 

114 invalids = [] 

115 for element_cls, ifc_entities in entity_class_dict.items(): 

116 for ifc_entity in ifc_entities: 

117 try: 

118 item = self.factory.create(element_cls, ifc_entity) 

119 element_lst.append(item) 

120 except Exception as ex: 

121 invalids.append(ifc_entity) 

122 if invalids: 

123 self.logger.info("Removed %d entities with no class set", 

124 len(invalids)) 

125 

126 self.logger.info(f"Created {len(element_lst)} bim2sim elements " 

127 f"based on IFC file {ifc_file.ifc_file_name}") 

128 elements.update({inst.guid: inst for inst in element_lst}) 

129 if not elements: 

130 self.logger.error("No bim2sim elements could be created based on " 

131 "the IFC files.") 

132 raise AssertionError("No bim2sim elements could be created, program" 

133 "will be terminated as no further process is " 

134 "possible.") 

135 self.logger.info(f"Created {len(elements)} bim2sim elements in " 

136 f"total for all IFC files.") 

137 # sort elements for easier handling 

138 elements = dict(sorted(elements.items())) 

139 # store copy of elements to preserve for alter operations 

140 _initial_elements = copy.copy(elements) 

141 return elements, _initial_elements, ifc_files 

142 

143 def create_with_validation(self, entities_dict: dict, warn=True, force=False) -> \ 

144 Tuple[List[ProductBased], List[Any]]: 

145 """Instantiate ifc_entities using given element class. 

146 

147 The given ifc entities are used to create bim2sim elements via factory 

148 method. After the creation the associated layers and material are 

149 created (see create_layers_and_materials). 

150 All created elements (including material and layers) are checked 

151 against the provided conditions and classified into valid and invalid. 

152 

153 Args: 

154 entities_dict: dict with ifc entities 

155 warn: boolean to warn if something condition fail 

156 force: boolean if conditions should be ignored 

157 

158 Returns: 

159 valid: list of all valid items that fulfill the conditions 

160 invalid: list of all elements that do not fulfill the conditions 

161 

162 """ 

163 valid, invalid = [], [] 

164 blacklist = [ 

165 'IfcMaterialLayerSet', 

166 'IfcMaterialLayer', 

167 'IfcMaterial', 

168 'IfcMaterialConstituentSet', 

169 'IfcMaterialConstituent', 

170 'IfcMaterialProfile', 

171 'IfcMaterialProfileSet', 

172 ] 

173 

174 blacklist_classes = [ 

175 bps.LayerSet, bps.Layer, Material 

176 ] 

177 

178 for entity, ifc_type_or_element_cls in entities_dict.items(): 

179 try: 

180 if isinstance(ifc_type_or_element_cls, str): 

181 if ifc_type_or_element_cls in blacklist: 

182 continue 

183 try: 

184 element = self.factory( 

185 entity, ifc_type=ifc_type_or_element_cls, 

186 use_dummy=False) 

187 except IFCDomainError: 

188 continue 

189 else: 

190 if ifc_type_or_element_cls in blacklist_classes: 

191 continue 

192 element = self.factory.create( 

193 ifc_type_or_element_cls, entity) 

194 except LookupError: 

195 invalid.append(entity) 

196 continue 

197 # TODO #676 

198 plugin_name = self.playground.project.plugin_cls.name 

199 if plugin_name in ['EnergyPlus', 'Comfort', 'Teaser']: 

200 if (self.playground.sim_settings.layers_and_materials 

201 is not LOD.low): 

202 raise NotImplementedError( 

203 "Only layers_and_materials using LOD.low is currently supported.") 

204 self.create_layers_and_materials(element) 

205 valid += ( 

206 self.layersets_all 

207 + self.layers_all + 

208 self.materials_all 

209 ) 

210 

211 if element.validate_creation(): 

212 valid.append(element) 

213 elif force: 

214 valid.append(element) 

215 if warn: 

216 self.logger.warning("Force accept invalid element %s %s", 

217 ifc_type_or_element_cls, element) 

218 else: 

219 if warn: 

220 self.logger.warning("Validation failed for %s %s", 

221 ifc_type_or_element_cls, element) 

222 invalid.append(entity) 

223 

224 return list(set(valid)), list(set(invalid)) 

225 

226 def create_layers_and_materials(self, element: Element): 

227 """Create all layers and materials associated with the given element. 

228 

229 Layers and materials are no IfcProducts and have no GUID. 

230 They are always associated to IfcProducts. To create the association 

231 between Product and layer or material we create layers and materials 

232 directly when creating the corresponding element and not directly based 

233 on their IFC type in the normal creation process. 

234 For more information how materials work in IFC have a look at 

235 

236 `wiki.osarch.org`_. 

237 _wiki.osarch.org: https://wiki.osarch.org/index.php?title=IFC_-_ 

238 Industry_Foundation_Classes/IFC_materials 

239 

240 Args: 

241 element: the already created bim2sim element 

242 """ 

243 quality_logger = logging.getLogger( 

244 'bim2sim.QualityReport') 

245 if hasattr(element.ifc, 'HasAssociations'): 

246 for association in element.ifc.HasAssociations: 

247 if association.is_a("IfcRelAssociatesMaterial"): 

248 ifc_mat_rel_object = association.RelatingMaterial 

249 

250 # Layers 

251 ifc_layerset_entity = None 

252 if ifc_mat_rel_object.is_a('IfcMaterialLayerSetUsage'): 

253 ifc_layerset_entity = ifc_mat_rel_object.ForLayerSet 

254 elif ifc_mat_rel_object.is_a('IfcMaterialLayerSet'): 

255 ifc_layerset_entity = ifc_mat_rel_object 

256 if ifc_layerset_entity: 

257 self.create_layersets(element, ifc_layerset_entity) 

258 

259 # Constituent sets 

260 if ifc_mat_rel_object.is_a( 

261 'IfcMaterialConstituentSet'): 

262 ifc_material_constituents =\ 

263 ifc_mat_rel_object.MaterialConstituents 

264 self.create_constituent( 

265 element, ifc_material_constituents, quality_logger) 

266 

267 # Direct Material 

268 if ifc_mat_rel_object.is_a('IfcMaterial'): 

269 ifc_material_entity = ifc_mat_rel_object 

270 material = self.create_material(ifc_material_entity) 

271 element.material = material 

272 material.parents.append(element) 

273 

274 # TODO maybe use in future 

275 # Profiles 

276 if ifc_mat_rel_object.is_a( 

277 'IfcMaterialProfileSetUsage'): 

278 pass 

279 elif ifc_mat_rel_object.is_a( 

280 'IfcMaterialProfileSet'): 

281 pass 

282 elif ifc_mat_rel_object.is_a( 

283 'IfcMaterialProfile'): 

284 pass 

285 

286 def create_layersets(self, element: Element, ifc_layerset_entity: entity_instance): 

287 """Instantiate the layerset and its layers and materials and link to 

288 element. 

289 

290 Layersets in IFC are used to describe the layer structure of e.g. walls. 

291 

292 Args: 

293 element: bim2sim element 

294 ifc_layerset_entity: ifc entity of layerset 

295 """ 

296 for layerset in self.layersets_all: 

297 if ifc_layerset_entity == layerset.ifc: 

298 break 

299 else: 

300 layerset = self.factory( 

301 ifc_layerset_entity, 

302 ifc_type='IfcMaterialLayerSet', 

303 use_dummy=False) 

304 self.layersets_all.append(layerset) 

305 

306 for ifc_layer_entity in ifc_layerset_entity.MaterialLayers: 

307 layer = self.factory( 

308 ifc_layer_entity, 

309 ifc_type='IfcMaterialLayer', 

310 use_dummy=False) 

311 self.layers_all.append(layer) 

312 layer.to_layerset.append(layerset) 

313 layerset.layers.append(layer) 

314 ifc_material_entity = ifc_layer_entity.Material 

315 material = self.create_material(ifc_material_entity) 

316 layer.material = material 

317 material.parents.append(layer) 

318 # add layerset to element and vice versa 

319 element.layerset = layerset 

320 layerset.parents.append(element) 

321 

322 def create_constituent( 

323 self, element: Element, ifc_material_constituents: entity_instance, quality_logger: Element): # Error durch mypy: Element has no attribute Layerset 

324 """Instantiate the constituent set and its materials and link to 

325 element. 

326 

327 Constituent sets in IFC are used to describe the e.g. windows which 

328 consist out of different materials (glass, frame etc.) or mixtures like 

329 concrete (sand, cement etc.). 

330 

331 Args: 

332 element: bim2sim element 

333 ifc_material_constituents: ifc entity of layerset 

334 quality_logger: element of bim2sim quality logger 

335 """ 

336 for ifc_constituent in ifc_material_constituents: 

337 ifc_material_entity = ifc_constituent.Material 

338 

339 material = self.create_material(ifc_material_entity) 

340 fraction = ifc_constituent.Fraction 

341 # todo if every element of the constituent has a geometric 

342 # representation we could use them to get the volume of the 

343 # different sub constituents and create fractions from it 

344 if not fraction: 

345 quality_logger.warning( 

346 f"{element} has a " 

347 f"IfcMaterialConstituentSet but no" 

348 f" information to fraction is provided") 

349 n = len(element.material_set) 

350 fraction = 'unknown_' + str(n) 

351 element.material_set[fraction] = material 

352 material.parents.append(element) 

353 

354 def create_material(self, ifc_material_entity: entity_instance): 

355 """As materials are unique in IFC we only want to have on material 

356 instance per material.""" 

357 for material in self.materials_all: 

358 if ifc_material_entity == material.ifc: 

359 break 

360 else: 

361 material = self.factory( 

362 ifc_material_entity, 

363 ifc_type='IfcMaterial', 

364 use_dummy=False) 

365 self.materials_all.append(material) 

366 return material 

367 

368 def filter_by_text(self, text_filter: TextFilter, ifc_entities: entity_instance, ifc_units: dict) \ 

369 -> Generator[DecisionBunch, None, 

370 Tuple[Dict[Any, Type[ProductBased]], List]]: 

371 """Generator method filtering ifc elements by given TextFilter. 

372 

373 yields decision bunch for ambiguous results""" 

374 entities_dict, unknown_entities = text_filter.run(ifc_entities) 

375 answers = {} 

376 decisions = DecisionBunch() 

377 for entity, classes in entities_dict.items(): 

378 sorted_classes = sorted(classes, key=lambda item: item.key) 

379 if len(sorted_classes) > 1: 

380 # choices 

381 choices = [] 

382 for element_cls in sorted_classes: 

383 # TODO: filter_for_text_fragments() 

384 # already called in text_filter.run() 

385 hints = f"Matches: '" + "', '".join( 

386 element_cls.filter_for_text_fragments( 

387 entity, ifc_units)) + "'" 

388 choices.append([element_cls.key, hints]) 

389 choices.append(["Other", "Other"]) 

390 decisions.append(ListDecision( 

391 question=f"Searching for text fragments in '{entity.Name}'," 

392 f" gave the following class hints. " 

393 f"Please select best match.", 

394 console_identifier=f"Name: '{entity.Name}', " 

395 f"Description: '{entity.Description}'", 

396 choices=choices, 

397 key=entity, 

398 related=[entity.GlobalId], 

399 global_key="TextFilter:%s.%s.%s" % ( 

400 entity.is_a(), entity.GlobalId, entity.Name), 

401 allow_skip=True, 

402 context=[entity.GlobalId])) 

403 elif len(sorted_classes) == 1: 

404 answers[entity] = sorted_classes[0].key 

405 # empty classes are covered below 

406 yield decisions 

407 answers.update(decisions.to_answer_dict()) 

408 result_entity_dict = {} 

409 for ifc_entity, element_classes in entities_dict.items(): 

410 element_key = answers.get(ifc_entity) 

411 element_cls = ProductBased.key_map.get(element_key) 

412 if element_cls: 

413 result_entity_dict[ifc_entity] = element_cls 

414 else: 

415 unknown_entities.append(ifc_entity) 

416 

417 return result_entity_dict, unknown_entities 

418 

419 def set_class_by_user( 

420 self, 

421 unknown_entities: list, 

422 sim_settings: BaseSimSettings, 

423 best_guess_dict: dict): 

424 """Ask user for every given ifc_entity to specify matching element 

425 class. 

426 

427 This function allows to define unknown classes based on user feedback. 

428 To reduce the number of decisions we implemented fuzzy search. If and 

429 how fuzzy search is used can be set the sim_settings 

430 group_unidentified and fuzzy_threshold. See group_similar_entities() 

431 for more information. 

432 

433 Args: 

434 unknown_entities: list of unknown entities 

435 sim_settings: sim_settings used for this project 

436 best_guess_dict: dict that holds the best guesses for every element 

437 """ 

438 

439 def group_similar_entities( 

440 search_type: str = 'fuzzy', 

441 fuzzy_threshold: float = 0.7) -> dict: 

442 """Group unknown entities to reduce number of decisions. 

443 

444 IFC elements are often not correctly specified, or have uncertain 

445 specifications like "USERDEFINED" as predefined type. For some IFC 

446 files this would lead to a very high amount of decisions to identify 

447 elements. To reduce those decisions, this function groups similar 

448 elements based on: 

449 - same name (exact) 

450 - similar name (fuzzy search) 

451 

452 Args: 

453 search_type: str which is either 'fuzzy' or 'name' 

454 fuzzy_threshold: float that sets the threshold for fuzzy search. 

455 A low threshold means a small similarity is required for 

456 grouping 

457 

458 Returns: 

459 representatives: A dict with a string of the representing ifc 

460 element type as key (e.g. 'IfcPipeFitting') and a list of all 

461 represented ifc elements. 

462 """ 

463 entities_by_type = {} 

464 for entity in unknown_entities: 

465 entity_type = entity.is_a() 

466 if entity_type not in entities_by_type: 

467 entities_by_type[entity_type] = [entity] 

468 else: 

469 entities_by_type[entity_type].append(entity) 

470 

471 representatives = {} 

472 for entity_type, entities in entities_by_type.items(): 

473 if len(entities) == 1: 

474 representatives.setdefault(entity_type, 

475 {entities[0]: entities}) 

476 continue 

477 # group based on similarity in string of "Name" of IFC element 

478 if search_type == 'fuzzy': 

479 # use names of entities for grouping 

480 representatives[entity_type] = group_by_levenshtein( 

481 entities, similarity_score=fuzzy_threshold) 

482 self.logger.info( 

483 f"Grouping the unidentified elements with fuzzy search " 

484 f"based on their Name (Threshold = {fuzzy_threshold})" 

485 f" reduced the number of unknown " 

486 f"entities from {len(entities_by_type[entity_type])} " 

487 f"elements of IFC type {entity_type} " 

488 f"to {len(representatives[entity_type])} elements.") 

489 # just group based on exact same string in "Name" of IFC element 

490 elif search_type == 'name': 

491 representatives[entity_type] = {} 

492 for entity in entities: 

493 # find if a key entity with same Name exists already 

494 repr_entity = None 

495 for repr in representatives[entity_type].keys(): 

496 if repr.Name == entity.Name: 

497 repr_entity = repr 

498 break 

499 

500 if not repr_entity: 

501 representatives[entity_type][entity] = [entity] 

502 else: 

503 representatives[entity_type][repr_entity].append(entity) 

504 self.logger.info( 

505 f"Grouping the unidentified elements by their Name " 

506 f"reduced the number of unknown entities from" 

507 f" {len(entities_by_type[entity_type])} " 

508 f"elements of IFC type {entity_type} " 

509 f"to {len(representatives[entity_type])} elements.") 

510 else: 

511 raise NotImplementedError('Only fuzzy and name grouping are' 

512 'implemented for now.') 

513 

514 return representatives 

515 

516 possible_elements = sim_settings.relevant_elements 

517 sorted_elements = sorted(possible_elements, key=lambda item: item.key) 

518 

519 result_entity_dict = {} 

520 ignore = [] 

521 

522 representatives = group_similar_entities( 

523 sim_settings.group_unidentified, sim_settings.fuzzy_threshold) 

524 

525 for ifc_type, repr_entities in sorted(representatives.items()): 

526 decisions = DecisionBunch() 

527 for ifc_entity, represented in repr_entities.items(): 

528 # assert same list of ifc_files 

529 checksum = Decision.build_checksum( 

530 [pe.key for pe in sorted_elements]) 

531 

532 best_guess_cls = best_guess_dict.get(ifc_entity) 

533 best_guess = best_guess_cls.key if best_guess_cls else None 

534 context = [] 

535 for port in ifc2python.get_ports(ifc_entity): 

536 connected_ports = ifc2python.get_ports_connections(port) 

537 con_ports_guid = [con.GlobalId for con in connected_ports] 

538 parents = [] 

539 for con_port in connected_ports: 

540 parents.extend(ifc2python.get_ports_parent(con_port)) 

541 parents_guid = [par.GlobalId for par in parents] 

542 context.append(port.GlobalId) 

543 context.extend(con_ports_guid + parents_guid) 

544 representative_global_keys = [] 

545 for represent in representatives[ifc_type][ifc_entity]: 

546 representative_global_keys.append( 

547 "SetClass:%s.%s.%s" % ( 

548 represent.is_a(), represent.GlobalId, 

549 represent.Name 

550 ) 

551 ) 

552 decisions.append(ListDecision( 

553 question="Found unidentified Element of %s" % ( 

554 ifc_entity.is_a()), 

555 console_identifier="Name: %s, Description: %s, GUID: %s, " 

556 "Predefined Type: %s" 

557 % (ifc_entity.Name, ifc_entity.Description, 

558 ifc_entity.GlobalId, ifc_entity.PredefinedType), 

559 choices=[ele.key for ele in sorted_elements], 

560 related=[ifc_entity.GlobalId], 

561 context=context, 

562 default=best_guess, 

563 key=ifc_entity, 

564 global_key="SetClass:%s.%s.%s" % ( 

565 ifc_entity.is_a(), ifc_entity.GlobalId, ifc_entity.Name 

566 ), 

567 representative_global_keys=representative_global_keys, 

568 allow_skip=True, 

569 validate_checksum=checksum)) 

570 self.logger.info(f"Found {len(decisions)} " 

571 f"unidentified Elements of IFC type {ifc_type} " 

572 f"to check by user") 

573 yield decisions 

574 

575 answers = decisions.to_answer_dict() 

576 

577 for ifc_entity, element_key in answers.items(): 

578 represented_entities = representatives[ifc_type][ifc_entity] 

579 if element_key is None: 

580 # todo check 

581 # ignore.append(ifc_entity) 

582 ignore.extend(represented_entities) 

583 else: 

584 element_cls = ProductBased.key_map[element_key] 

585 lst = result_entity_dict.setdefault(element_cls, []) 

586 # lst.append(ifc_entity) 

587 lst.extend(represented_entities) 

588 

589 return result_entity_dict, ignore 

590 

591 def get_ifc_types(self, relevant_elements: List[Type[ProductBased]]) \ 

592 -> Set[str]: 

593 """Extract used ifc types from list of elements.""" 

594 relevant_ifc_types = [] 

595 for ele in relevant_elements: 

596 relevant_ifc_types.extend(ele.ifc_types.keys()) 

597 return set(relevant_ifc_types)