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
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1from __future__ import annotations
3import logging
4from typing import Tuple, List, Any, Generator, Dict, Type, Set
5import copy
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
22class CreateElementsOnIfcTypes(ITask):
23 """Create bim2sim elements based on information of IFC types."""
25 reads = ('ifc_files',)
26 touches = ('elements', '_initial_elements', 'ifc_files')
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 = []
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.
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.
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)
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)
72 # Filtering:
73 # filter returns dict of entities: suggested class and list of unknown
74 # accept_valids returns created elements and lst of invalids
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)
82 # create valid elements
83 valids, invalids = self.create_with_validation(entity_type_dict)
84 element_lst.extend(valids)
85 unknown_entities.extend(invalids)
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)
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))
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))
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
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.
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.
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
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
162 """
163 valid, invalid = [], []
164 blacklist = [
165 'IfcMaterialLayerSet',
166 'IfcMaterialLayer',
167 'IfcMaterial',
168 'IfcMaterialConstituentSet',
169 'IfcMaterialConstituent',
170 'IfcMaterialProfile',
171 'IfcMaterialProfileSet',
172 ]
174 blacklist_classes = [
175 bps.LayerSet, bps.Layer, Material
176 ]
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 )
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)
224 return list(set(valid)), list(set(invalid))
226 def create_layers_and_materials(self, element: Element):
227 """Create all layers and materials associated with the given element.
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
236 `wiki.osarch.org`_.
237 _wiki.osarch.org: https://wiki.osarch.org/index.php?title=IFC_-_
238 Industry_Foundation_Classes/IFC_materials
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
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)
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)
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)
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
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.
290 Layersets in IFC are used to describe the layer structure of e.g. walls.
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)
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)
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.
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.).
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
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)
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
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.
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)
417 return result_entity_dict, unknown_entities
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.
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.
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 """
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.
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)
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
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)
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
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.')
514 return representatives
516 possible_elements = sim_settings.relevant_elements
517 sorted_elements = sorted(possible_elements, key=lambda item: item.key)
519 result_entity_dict = {}
520 ignore = []
522 representatives = group_similar_entities(
523 sim_settings.group_unidentified, sim_settings.fuzzy_threshold)
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])
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
575 answers = decisions.to_answer_dict()
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)
589 return result_entity_dict, ignore
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)