Coverage for bim2sim/tasks/common/create_elements.py: 59%
293 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 08:28 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 08:28 +0000
1from __future__ import annotations
3import collections
4import logging
5from typing import Tuple, List, Any, Generator, Dict, Type, Set
6import copy
8from bim2sim.elements import bps_elements as bps
9from bim2sim.elements.base_elements import (Factory, ProductBased, Material,
10 Element)
11from bim2sim.elements.mapping import ifc2python
12from bim2sim.elements.mapping.filter import (TypeFilter, TextFilter,
13 StoreyFilter)
14from bim2sim.kernel import IFCDomainError
15from bim2sim.kernel.decision import DecisionBunch, ListDecision, Decision
16from bim2sim.kernel.ifc_file import IfcFileClass
17from bim2sim.sim_settings import BaseSimSettings
18from bim2sim.tasks.base import ITask
19from bim2sim.utilities.common_functions import group_by_levenshtein
20from bim2sim.utilities.types import LOD
21from bim2sim.tasks.base import Playground
22from ifcopenshell import file, entity_instance
25class CreateElementsOnIfcTypes(ITask):
26 """Create bim2sim elements based on information of IFC types."""
28 reads = ('ifc_files',)
29 touches = ('elements', '_initial_elements', 'ifc_files')
31 def __init__(self, playground: Playground):
32 super().__init__(playground)
33 self.factory = None
34 self.source_tools: list = []
35 self.layersets_all: list = []
36 self.materials_all: list = []
37 self.layers_all: list = []
39 def run(self, ifc_files: [IfcFileClass]) -> (
40 Tuple)[Dict[Any, Element], Dict[Any, Element], List[IfcFileClass]]:
41 """This task creates the bim2sim elements based on the ifc data.
43 For each ifc file a factory instance is created. The factory instance
44 allows the easy creation of bim2sim elements based on ifc elements.
45 As we might not want to create bim2sim elements for every existing ifc
46 element, we use the concept of relevant_elements which are taken from
47 the sim_setting relevant_elements. This way the user can describe which
48 bim2sim elements are relevant for the respective simulation and only
49 the fitting ifc elements are taken into account.
50 During the creation of the bim2sim elements validations are performed,
51 to make sure that the resulting bim2sim elements hold valid
52 information.
54 The element creation process follows a three-stage classification
55 approach:
57 1. **Validation-Based Classification**:
58 * Elements are initially processed based on relevant element
59 types and IFC classifications
60 * Each element undergoes validation against predefined criteria
61 * Valid elements are immediately added to the "valids" collection
62 * Elements failing validation are placed in "unknown_entities"
63 for further processing
65 2. **Pattern Matching Classification**:
66 * For unidentified elements in "unknown_entities", a text
67 analysis is performed
68 * The system examines IFC descriptions and compares them against
69 regular expression patterns defined in element classes
70 * Results are handled based on match confidence:
71 - Single match: Element is automatically moved to "valids"
72 - Multiple matches: User decision is requested to determine the
73 correct classification
74 - No matches: Element remains in "unknown_entities" for final
75 classification stage
77 3. **User-Assisted Classification**:
78 * Remaining unidentified elements are processed through the
79 set_class_by_user function
80 * To optimize user experience, similar elements are intelligently
81 grouped using one of these strategies:
82 - Exact name matching: Groups elements with identical names
83 - Name and description matching: Groups elements with identical
84 names and descriptions
85 - Fuzzy matching: Groups elements with similar names based on a
86 configurable similarity threshold
87 * This grouping significantly reduces the number of required user
88 decisions
89 * User decisions determine the final classification of each
90 element or group
92 Args:
93 ifc_files: list of ifc files in bim2sim structured format
94 Returns:
95 elements: bim2sim elements created based on ifc data
96 ifc_files: list of ifc files in bim2sim structured format
97 """
98 self.logger.info("Creates elements of relevant ifc types")
99 default_ifc_types = {'IfcBuildingElementProxy', 'IfcUnitaryEquipment'}
100 # Todo maybe move this into IfcFileClass instead simulation settings
101 relevant_elements = self.playground.sim_settings.relevant_elements
102 relevant_ifc_types = self.get_ifc_types(relevant_elements)
103 relevant_ifc_types.update(default_ifc_types)
105 elements: dict = {}
106 for ifc_file in ifc_files:
107 self.factory = Factory(
108 relevant_elements,
109 ifc_file.ifc_units,
110 ifc_file.domain,
111 ifc_file.finder)
113 # Filtering:
114 # filter returns dict of entities: suggested class and list of
115 # unknown accept_valids returns created elements and lst of
116 # invalids
118 element_lst: list = []
119 entity_best_guess_dict: dict = {}
120 # filter by type
121 type_filter = TypeFilter(relevant_ifc_types)
122 entity_type_dict, unknown_entities = type_filter.run(
123 ifc_file.file)
125 # Extract stories if sim_setting is active
126 if self.playground.sim_settings.stories_to_load_guids:
127 storey_filter = StoreyFilter(
128 self.playground.sim_settings.stories_to_load_guids)
129 entity_type_dict, unknown_entities = storey_filter.run(
130 ifc_file.file, entity_type_dict, unknown_entities)
132 # create valid elements
133 # First elements are created with validation, by checking
134 # conditions and number of ports if they fit expectations, those
135 # who don't fit expectations are added to invalids list
136 valids, invalids = self.create_with_validation(entity_type_dict)
137 element_lst.extend(valids)
138 unknown_entities.extend(invalids)
140 # filter by text
141 # invalids from before are checked with text filter on their IFC
142 # Description to identify them
143 text_filter = TextFilter(
144 relevant_elements,
145 ifc_file.ifc_units,
146 ['Description'])
147 entity_class_dict, unknown_entities = yield from (
148 self.filter_by_text(
149 text_filter, unknown_entities))
150 entity_best_guess_dict.update(entity_class_dict)
152 # Now we use the result of previous text filter to create the
153 # elements that could be identified by text filter
154 valids, invalids = self.create_with_validation(
155 entity_class_dict, force=True)
156 element_lst.extend(valids)
157 unknown_entities.extend(invalids)
159 self.logger.info("Found %d relevant elements", len(element_lst))
160 self.logger.info("Found %d ifc_entities that could not be "
161 "identified and therefore not converted into a"
162 " bim2sim element.",
163 len(unknown_entities))
165 # identification of remaining entities by user
166 # those are elements that failed initial validation and could not
167 # be identified by text filter
168 entity_class_dict, unknown_entities = yield from (
169 self.set_class_by_user(
170 unknown_entities,
171 self.playground.sim_settings,
172 entity_best_guess_dict)
173 )
174 entity_best_guess_dict.update(entity_class_dict)
175 invalids = []
177 for element_cls, ifc_entities in entity_class_dict.items():
178 for ifc_entity in ifc_entities:
179 try:
180 item = self.factory.create(element_cls, ifc_entity)
181 element_lst.append(item)
182 except Exception as ex:
183 invalids.append(ifc_entity)
184 if invalids:
185 self.logger.info("Removed %d entities with no class set",
186 len(invalids))
188 self.logger.info(f"Created {len(element_lst)} bim2sim elements "
189 f"based on IFC file {ifc_file.ifc_file_name}")
190 elements.update({inst.guid: inst for inst in element_lst})
191 if not elements:
192 self.logger.error("No bim2sim elements could be created based on "
193 "the IFC files.")
194 raise AssertionError(
195 "No bim2sim elements could be created, program"
196 "will be terminated as no further process is "
197 "possible.")
198 self.logger.info(f"Created {len(elements)} bim2sim elements in "
199 f"total for all IFC files.")
200 # sort elements for easier handling
201 elements = dict(sorted(elements.items()))
202 # store copy of elements to preserve for alter operations
203 _initial_elements = copy.copy(elements)
204 return elements, _initial_elements, ifc_files
206 def create_with_validation(self, entities_dict: dict, warn=True,
207 force=False) -> \
208 Tuple[List[ProductBased], List[Any]]:
209 """Instantiate ifc_entities using given element class.
211 The given ifc entities are used to create bim2sim elements via factory
212 method. After the creation the associated layers and material are
213 created (see create_layers_and_materials).
214 All created elements (including material and layers) are checked
215 against the provided conditions and classified into valid and invalid.
217 Args:
218 entities_dict: dict with ifc entities
219 warn: boolean to warn if something condition fail
220 force: boolean if conditions should be ignored
222 Returns:
223 valid: list of all valid items that fulfill the conditions
224 invalid: list of all elements that do not fulfill the conditions
226 """
227 valid, invalid = [], []
228 blacklist = [
229 'IfcMaterialLayerSet',
230 'IfcMaterialLayer',
231 'IfcMaterial',
232 'IfcMaterialConstituentSet',
233 'IfcMaterialConstituent',
234 'IfcMaterialProfile',
235 'IfcMaterialProfileSet',
236 ]
238 blacklist_classes = [
239 bps.LayerSet, bps.Layer, Material
240 ]
242 for entity, ifc_type_or_element_cls in entities_dict.items():
243 try:
244 if isinstance(ifc_type_or_element_cls, str):
245 if ifc_type_or_element_cls in blacklist:
246 continue
247 try:
248 element = self.factory(
249 entity, ifc_type=ifc_type_or_element_cls,
250 use_dummy=False)
251 except IFCDomainError:
252 continue
253 else:
254 if ifc_type_or_element_cls in blacklist_classes:
255 continue
256 element = self.factory.create(
257 ifc_type_or_element_cls, entity)
258 except LookupError:
259 invalid.append(entity)
260 continue
261 # TODO #676
262 plugin_name = self.playground.project.plugin_cls.name
263 if plugin_name in ['EnergyPlus', 'Comfort', 'Teaser']:
264 if (self.playground.sim_settings.layers_and_materials
265 is not LOD.low):
266 raise NotImplementedError(
267 "Only layers_and_materials using LOD.low is "
268 "currently supported.")
269 self.create_layers_and_materials(element)
270 valid += (
271 self.layersets_all
272 + self.layers_all +
273 self.materials_all
274 )
276 if element.validate_creation():
277 valid.append(element)
278 elif force:
279 valid.append(element)
280 if warn:
281 self.logger.warning("Force accept invalid element %s %s",
282 ifc_type_or_element_cls, element)
283 else:
284 if warn:
285 self.logger.warning("Validation failed for %s %s",
286 ifc_type_or_element_cls, element)
287 invalid.append(entity)
289 return list(set(valid)), list(set(invalid))
291 def create_layers_and_materials(self, element: Element):
292 """Create all layers and materials associated with the given element.
294 Layers and materials are no IfcProducts and have no GUID.
295 They are always associated to IfcProducts. To create the association
296 between Product and layer or material we create layers and materials
297 directly when creating the corresponding element and not directly based
298 on their IFC type in the normal creation process.
299 For more information how materials work in IFC have a look at
301 `wiki.osarch.org`_.
302 _wiki.osarch.org: https://wiki.osarch.org/index.php?title=IFC_-_
303 Industry_Foundation_Classes/IFC_materials
305 Args:
306 element: the already created bim2sim element
307 """
308 quality_logger = logging.getLogger(
309 'bim2sim.QualityReport')
310 if hasattr(element.ifc, 'HasAssociations'):
311 for association in element.ifc.HasAssociations:
312 if association.is_a("IfcRelAssociatesMaterial"):
313 ifc_mat_rel_object = association.RelatingMaterial
315 # Layers
316 ifc_layerset_entity = None
317 if ifc_mat_rel_object.is_a('IfcMaterialLayerSetUsage'):
318 ifc_layerset_entity = ifc_mat_rel_object.ForLayerSet
319 elif ifc_mat_rel_object.is_a('IfcMaterialLayerSet'):
320 ifc_layerset_entity = ifc_mat_rel_object
321 if ifc_layerset_entity:
322 self.create_layersets(element, ifc_layerset_entity)
324 # Constituent sets
325 if ifc_mat_rel_object.is_a(
326 'IfcMaterialConstituentSet'):
327 ifc_material_constituents = \
328 ifc_mat_rel_object.MaterialConstituents
329 self.create_constituent(
330 element, ifc_material_constituents, quality_logger)
332 # Direct Material
333 if ifc_mat_rel_object.is_a('IfcMaterial'):
334 ifc_material_entity = ifc_mat_rel_object
335 material = self.create_material(ifc_material_entity)
336 element.material = material
337 material.parents.append(element)
339 # TODO maybe use in future
340 # Profiles
341 if ifc_mat_rel_object.is_a(
342 'IfcMaterialProfileSetUsage'):
343 pass
344 elif ifc_mat_rel_object.is_a(
345 'IfcMaterialProfileSet'):
346 pass
347 elif ifc_mat_rel_object.is_a(
348 'IfcMaterialProfile'):
349 pass
351 def create_layersets(self, element: Element,
352 ifc_layerset_entity: entity_instance):
353 """Instantiate the layerset and its layers and materials and link to
354 element.
356 Layersets in IFC are used to describe the layer structure of e.g.
357 walls.
359 Args:
360 element: bim2sim element
361 ifc_layerset_entity: ifc entity of layerset
362 """
363 for layerset in self.layersets_all:
364 if ifc_layerset_entity == layerset.ifc:
365 break
366 else:
367 layerset = self.factory(
368 ifc_layerset_entity,
369 ifc_type='IfcMaterialLayerSet',
370 use_dummy=False)
371 self.layersets_all.append(layerset)
373 for ifc_layer_entity in ifc_layerset_entity.MaterialLayers:
374 layer = self.factory(
375 ifc_layer_entity,
376 ifc_type='IfcMaterialLayer',
377 use_dummy=False)
378 self.layers_all.append(layer)
379 layer.to_layerset.append(layerset)
380 layerset.layers.append(layer)
381 ifc_material_entity = ifc_layer_entity.Material
382 material = self.create_material(ifc_material_entity)
383 layer.material = material
384 material.parents.append(layer)
385 # add layerset to element and vice versa
386 element.layerset = layerset
387 layerset.parents.append(element)
389 def create_constituent(
390 self, element: Element, ifc_material_constituents: entity_instance,
391 quality_logger: Element): # Error durch mypy: Element has no
392 # attribute Layerset
393 """Instantiate the constituent set and its materials and link to
394 element.
396 Constituent sets in IFC are used to describe the e.g. windows which
397 consist out of different materials (glass, frame etc.) or mixtures like
398 concrete (sand, cement etc.).
400 Args:
401 element: bim2sim element
402 ifc_material_constituents: ifc entity of layerset
403 quality_logger: element of bim2sim quality logger
404 """
405 for ifc_constituent in ifc_material_constituents:
406 ifc_material_entity = ifc_constituent.Material
408 material = self.create_material(ifc_material_entity)
409 fraction = ifc_constituent.Fraction
410 # todo if every element of the constituent has a geometric
411 # representation we could use them to get the volume of the
412 # different sub constituents and create fractions from it
413 if not fraction:
414 quality_logger.warning(
415 f"{element} has a "
416 f"IfcMaterialConstituentSet but no"
417 f" information to fraction is provided")
418 n = len(element.material_set)
419 fraction = 'unknown_' + str(n)
420 element.material_set[fraction] = material
421 material.parents.append(element)
423 def create_material(self, ifc_material_entity: entity_instance):
424 """As materials are unique in IFC we only want to have on material
425 instance per material."""
426 for material in self.materials_all:
427 if ifc_material_entity == material.ifc:
428 break
429 else:
430 material = self.factory(
431 ifc_material_entity,
432 ifc_type='IfcMaterial',
433 use_dummy=False)
434 self.materials_all.append(material)
435 return material
437 def filter_by_text(self, text_filter: TextFilter,
438 ifc_entities: entity_instance) \
439 -> Generator[DecisionBunch, None, Tuple[
440 Dict[Any, Type[ProductBased]], List]]:
441 """Filter IFC elements by analyzing text fragments.
443 This method applies text-based filtering to identify elements. It
444 handles:
445 1. Automatic classification for entities with single matching class
446 2. User decision for entities with multiple potential matching classes
447 3. Collection of unidentified entities
449 Args:
450 text_filter: The TextFilter instance to use.
451 ifc_entities: IFC entities to filter.
453 Yields:
454 DecisionBunch: User decisions for ambiguous matches.
456 Returns:
457 tuple:
458 - Dictionary mapping entities to their identified element
459 classes
460 - List of unidentified entities
461 """
462 # Step 1: Run the text filter to find potential matches
463 # Now each entity maps to a dictionary of {class: fragments}
464 entities_match_dict, unknown_entities = text_filter.run(ifc_entities)
466 # Prepare containers for results
467 direct_matches = {} # For entities with a single match
468 decisions = DecisionBunch() # For entities requiring user decision
470 # Step 2: Process the matches
471 for entity, class_fragments_dict in entities_match_dict.items():
472 # Sort classes by their key
473 sorted_classes = sorted(class_fragments_dict.keys(),
474 key=lambda cls: cls.key)
476 if len(sorted_classes) == 1:
477 # Single match - direct assignment
478 direct_matches[entity] = sorted_classes[0]
479 elif len(sorted_classes) > 1:
480 # Multiple matches - user decision required
481 choices = []
483 # Create choices with helpful hints using already computed
484 # fragments
485 for element_cls in sorted_classes:
486 fragments = class_fragments_dict[element_cls]
487 hints = f"Matches: '{', '.join(fragments)}'"
488 choices.append([element_cls.key, hints])
490 choices.append(["Other", "Other"])
492 # Create decision
493 decisions.append(ListDecision(
494 question=f"Searching for text fragments in '"
495 f"{entity.Name}' gave the following class hints. "
496 f"Please select best match.",
497 console_identifier=f"Name: '{entity.Name}', Descr"
498 f"iption: '{entity.Description}'",
499 choices=choices,
500 key=entity,
501 related=[entity.GlobalId],
502 global_key=f"TextFilter:{entity.is_a()}"
503 f".{entity.GlobalId}.{entity.Name}",
504 allow_skip=True,
505 context=[entity.GlobalId]
506 ))
508 # Step 3: Yield decisions for user input
509 yield decisions
511 # Step 4: Process results
512 result_entity_dict = {}
514 # Add direct matches
515 for entity, element_cls in direct_matches.items():
516 result_entity_dict[entity] = element_cls
518 # Process user decisions
519 answers = decisions.to_answer_dict()
520 for entity, element_key in answers.items():
521 element_cls = ProductBased.key_map.get(element_key)
522 if element_cls:
523 result_entity_dict[entity] = element_cls
524 else:
525 unknown_entities.append(entity)
527 return result_entity_dict, unknown_entities
529 def set_class_by_user(
530 self,
531 unknown_entities: list,
532 sim_settings: BaseSimSettings,
533 best_guess_dict: dict):
534 """Ask user for every given ifc_entity to specify matching element
535 class.
537 This function allows to define unknown classes based on user feedback.
538 To reduce the number of decisions we implemented fuzzy search. If and
539 how fuzzy search is used can be set the sim_settings
540 group_unidentified and fuzzy_threshold. See group_similar_entities()
541 for more information.
543 Args:
544 unknown_entities: list of unknown entities
545 sim_settings: sim_settings used for this project
546 best_guess_dict: dict that holds the best guesses for every element
547 """
549 def group_similar_entities(
550 search_type: str = 'fuzzy',
551 fuzzy_threshold: float = 0.7) -> dict:
552 """Group unknown entities to reduce number of decisions.
554 IFC elements are often not correctly specified, or have uncertain
555 specifications like "USERDEFINED" as predefined type. For some IFC
556 files this would lead to a very high amount of decisions to
557 identify
558 elements. To reduce those decisions, this function groups similar
559 elements based on:
560 - same name (exact)
561 - similar name (fuzzy search)
563 Args:
564 search_type: str which is either 'fuzzy' or 'name'
565 fuzzy_threshold: float that sets the threshold for fuzzy
566 search.
567 A low threshold means a small similarity is required for
568 grouping
570 Returns:
571 representatives: A dict with a string of the representing ifc
572 element type as key (e.g. 'IfcPipeFitting') and a list of all
573 represented ifc elements.
574 """
575 # Sort unknown entities by GlobalId for determinism
576 unknown_entities_sorted = sorted(unknown_entities,
577 key=lambda e: e.GlobalId)
579 entities_by_type = {}
580 for entity in unknown_entities_sorted:
581 entity_type = entity.is_a()
582 if entity_type not in entities_by_type:
583 entities_by_type[entity_type] = [entity]
584 else:
585 entities_by_type[entity_type].append(entity)
587 representatives = {}
588 for entity_type, entities in sorted(entities_by_type.items()):
589 if len(entities) == 1:
590 # Use OrderedDict for stable iteration
591 entity_dict = collections.OrderedDict()
592 entity_dict[entities[0]] = entities
593 representatives[entity_type] = entity_dict
594 continue
596 # group based on similarity in string of "Name" of IFC element
597 if search_type == 'fuzzy':
598 # Modify group_by_levenshtein to ensure stable sorting
599 grouped = group_by_levenshtein(
600 entities, similarity_score=fuzzy_threshold)
602 # Convert result to a stable structure
603 stable_grouped = collections.OrderedDict()
604 for key, group in sorted(grouped.items(),
605 key=lambda x: x[0].GlobalId):
606 # Sort the group itself
607 stable_grouped[key] = sorted(group,
608 key=lambda e: e.GlobalId)
610 representatives[entity_type] = stable_grouped
612 self.logger.info(
613 f"Grouping the unidentified elements with fuzzy "
614 f"search "
615 f"based on their Name (Threshold = {fuzzy_threshold})"
616 f" reduced the number of unknown "
617 f"entities from {len(entities_by_type[entity_type])} "
618 f"elements of IFC type {entity_type} "
619 f"to {len(representatives[entity_type])} elements.")
621 # just group based on exact same string in "Name" of IFC
622 # element
623 elif search_type == 'name':
624 # Use OrderedDict for stable iteration
625 name_groups = collections.defaultdict(list)
627 # Group by name, but keep the sorting
628 for entity in entities:
629 name_groups[entity.Name].append(entity)
631 # Create the stable representatives dictionary
632 stable_grouped = collections.OrderedDict()
633 for name, group in sorted(name_groups.items()):
634 # Sort the group by GlobalId and choose the first
635 # element as representative
636 sorted_group = sorted(group, key=lambda e: e.GlobalId)
637 stable_grouped[sorted_group[0]] = sorted_group
639 representatives[entity_type] = stable_grouped
641 self.logger.info(
642 f"Grouping the unidentified elements by their Name "
643 f"reduced the number of unknown entities from"
644 f" {len(entities_by_type[entity_type])} "
645 f"elements of IFC type {entity_type} "
646 f"to {len(representatives[entity_type])} elements.")
647 elif search_type == 'name_and_description':
648 # Use OrderedDict for stable iteration
649 name_desc_groups = collections.defaultdict(list)
651 # Group by name AND description, but keep the sorting
652 for entity in entities:
653 # Create a composite key combining Name and
654 # Description, handling None values
655 name = entity.Name if (
656 hasattr(entity, "Name") and
657 entity.Name is not None) else ""
658 desc = entity.Description if (
659 hasattr(entity, "Description") and
660 entity.Description is not None) else ""
661 composite_key = (name, desc)
662 name_desc_groups[composite_key].append(entity)
664 # Create the stable representatives dictionary
665 stable_grouped = collections.OrderedDict()
667 # Sort by composite key, handling None values by
668 # converting them to empty strings
669 for composite_key, group in sorted(
670 name_desc_groups.items()):
671 # Sort the group by GlobalId and choose the first
672 # element as representative
673 sorted_group = sorted(group, key=lambda e: e.GlobalId)
674 stable_grouped[sorted_group[0]] = sorted_group
676 representatives[entity_type] = stable_grouped
678 self.logger.info(
679 f"Grouping the unidentified elements by their Name "
680 f"and Description "
681 f"reduced the number of unknown entities from"
682 f" {len(entities_by_type[entity_type])} "
683 f"elements of IFC type {entity_type} "
684 f"to {len(representatives[entity_type])} elements.")
685 else:
686 raise NotImplementedError(
687 'Only fuzzy and name grouping are'
688 'implemented for now.')
690 return representatives
692 possible_elements = sim_settings.relevant_elements
693 sorted_elements = sorted(possible_elements, key=lambda item: item.key)
695 result_entity_dict = {}
696 ignore = []
698 representatives = group_similar_entities(
699 sim_settings.group_unidentified, sim_settings.fuzzy_threshold)
701 # Sort the outer loop by IFC type
702 # Create a single decision bunch for all entities
703 all_decisions = DecisionBunch()
704 # Store associations between decisions and their representatives
705 decision_associations = {} # Maps (decision -> (ifc_type, ifc_entity))
707 # Sort the outer loop by IFC type
708 for ifc_type, repr_entities in sorted(representatives.items()):
709 # Sort the inner loop by GlobalId for determinism
710 for ifc_entity, represented in sorted(repr_entities.items(),
711 key=lambda x: x[0].GlobalId):
712 # assert same list of ifc_files
713 checksum = Decision.build_checksum(
714 [pe.key for pe in sorted_elements])
716 best_guess_cls = best_guess_dict.get(ifc_entity)
717 best_guess = best_guess_cls.key if best_guess_cls else None
719 # Sort ports and related elements
720 context = []
721 for port in sorted(ifc2python.get_ports(ifc_entity),
722 key=lambda p: p.GlobalId):
723 connected_ports = sorted(
724 ifc2python.get_ports_connections(port),
725 key=lambda p: p.GlobalId)
726 con_ports_guid = [con.GlobalId for con in connected_ports]
727 parents = []
728 for con_port in sorted(connected_ports,
729 key=lambda p: p.GlobalId):
730 port_parents = sorted(
731 ifc2python.get_ports_parent(con_port),
732 key=lambda p: p.GlobalId)
733 parents.extend(port_parents)
734 parents_guid = [par.GlobalId for par in parents]
735 context.append(port.GlobalId)
736 context.extend(sorted(con_ports_guid + parents_guid))
738 # Sort the represented elements
739 representative_global_keys = []
740 for represent in sorted(repr_entities[ifc_entity],
741 key=lambda e: e.GlobalId):
742 representative_global_keys.append(
743 "SetClass:%s.%s.%s" % (
744 represent.is_a(), represent.GlobalId,
745 represent.Name
746 )
747 )
749 decision = ListDecision(
750 question="Found unidentified Element of Ifc Type: %s" % (
751 ifc_entity.is_a()),
752 console_identifier="Name: %s, Description: %s, GUID: %s, "
753 "Predefined Type: %s"
754 % (
755 ifc_entity.Name,
756 ifc_entity.Description,
757 ifc_entity.GlobalId,
758 ifc_entity.PredefinedType),
759 choices=[ele.key for ele in sorted_elements],
760 related=[ifc_entity.GlobalId],
761 context=sorted(context), # Sort the context
762 default=best_guess,
763 key=ifc_entity,
764 global_key="SetClass:%s.%s.%s" % (
765 ifc_entity.is_a(), ifc_entity.GlobalId, ifc_entity.Name
766 ),
767 representative_global_keys=sorted(
768 representative_global_keys), # Sort the keys
769 allow_skip=True,
770 validate_checksum=checksum)
772 # Add decision to the bunch
773 all_decisions.append(decision)
775 # Store association between decision and its representatives
776 decision_associations[ifc_entity] = (ifc_type, ifc_entity)
778 self.logger.info(
779 f"Found {len(all_decisions)} unidentified Elements to check by "
780 f"user")
782 # Yield the single big bunch of decisions
783 yield all_decisions
785 # Process all answers at once
786 answers = all_decisions.to_answer_dict()
788 for ifc_entity, element_key in sorted(answers.items(),
789 key=lambda x: x[0].GlobalId):
790 # Get the associated ifc_type and representative entity
791 ifc_type, _ = decision_associations[ifc_entity]
792 represented_entities = representatives[ifc_type][ifc_entity]
794 if element_key is None:
795 ignore.extend(
796 sorted(represented_entities, key=lambda e: e.GlobalId))
797 else:
798 element_cls = ProductBased.key_map[element_key]
799 lst = result_entity_dict.setdefault(element_cls, [])
800 lst.extend(
801 sorted(represented_entities, key=lambda e: e.GlobalId))
803 return result_entity_dict, ignore
805 @staticmethod
806 def get_ifc_types(relevant_elements: List[Type[ProductBased]]) \
807 -> Set[str]:
808 """Extract used ifc types from list of elements."""
809 relevant_ifc_types = []
810 for ele in relevant_elements:
811 relevant_ifc_types.extend(ele.ifc_types.keys())
812 return set(relevant_ifc_types)