Coverage for bim2sim/tasks/bps/sb_creation.py: 18%
188 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
1import logging
2import math
3from typing import List, Union, Tuple, Dict
5from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex
6from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
7from OCC.Core.Extrema import Extrema_ExtFlag_MIN
8from OCC.Core.gp import gp_Pnt, gp_Dir
10from bim2sim.elements.mapping.filter import TypeFilter
11from bim2sim.elements.base_elements import RelationBased, Element, IFCBased
12from bim2sim.elements.bps_elements import (
13 SpaceBoundary, ExtSpatialSpaceBoundary, ThermalZone, Window, Door,
14 BPSProductWithLayers)
15from bim2sim.elements.mapping.finder import TemplateFinder
16from bim2sim.elements.mapping.units import ureg
17from bim2sim.tasks.base import ITask
18from bim2sim.sim_settings import BaseSimSettings
19from bim2sim.utilities.common_functions import (
20 get_spaces_with_bounds, all_subclasses)
22logger = logging.getLogger(__name__)
25class CreateSpaceBoundaries(ITask):
26 """Create space boundary elements from ifc.
28 See run function for further information on this module. """
30 reads = ('ifc_files', 'elements')
32 def run(self, ifc_files: list, elements: dict):
33 """Create space boundaries for elements from IfcRelSpaceBoundary.
35 This module contains all functions for setting up bim2sim elements of
36 type SpaceBoundary based on the IFC elements IfcRelSpaceBoundary and
37 their subtypes of IfcRelSpaceBoundary2ndLevel.
38 Within this module, bim2sim SpaceBoundary instances are created.
39 Additionally, the relationship to their parent elements (i.e.,
40 related IfcProduct-based bim2sim elements, such as IfcWalls or
41 IfcRoof) is assigned. The SpaceBoundary instances are added to the
42 dictionary of space_boundaries in the format {guid:
43 bim2sim SpaceBoundary} and returned.
45 Args:
46 ifc_files (list): list of ifc files that have to be processed.
47 elements (dict): dictionary of preprocessed bim2sim elements (
48 generated from IFC or from other enrichment processes.
49 space_boundaries (dict): dictionary in the format dict[guid:
50 SpaceBoundary], dictionary of IFC-based space boundary elements.
51 """
53 if not self.playground.sim_settings.add_space_boundaries:
54 return
55 logger.info("Creates elements for IfcRelSpaceBoundarys")
56 type_filter = TypeFilter(('IfcRelSpaceBoundary',))
57 space_boundaries = {}
58 for ifc_file in ifc_files:
59 entity_type_dict, unknown_entities = type_filter.run(ifc_file.file)
60 bound_list = self.instantiate_space_boundaries(
61 entity_type_dict, elements, ifc_file.finder,
62 self.playground.sim_settings.create_external_elements,
63 ifc_file.ifc_units)
64 bound_elements = self.get_parents_and_children(
65 self.playground.sim_settings, bound_list, elements)
66 bound_list = list(bound_elements.values())
67 logger.info(f"Created {len(bound_elements)} bim2sim SpaceBoundary "
68 f"elements based on IFC file: {ifc_file.ifc_file_name}")
69 space_boundaries.update({inst.guid: inst for inst in bound_list})
70 logger.info(f"Created {len(space_boundaries)} bim2sim SpaceBoundary "
71 f"elements in total for all IFC files.")
73 self.add_bounds_to_elements(elements, space_boundaries)
74 self.remove_elements_without_sbs(elements)
76 @staticmethod
77 def remove_elements_without_sbs(elements: dict):
78 """Remove elements that hold no Space Boundaries.
80 Those elements are usual not relevant for the simulation.
81 """
82 elements_to_remove = []
83 for ele in elements.values():
84 if not any([isinstance(ele, bps_product_layer_ele) for
85 bps_product_layer_ele in
86 all_subclasses(BPSProductWithLayers)]):
87 continue
88 if not ele.space_boundaries:
89 elements_to_remove.append(ele.guid)
90 for ele_guid_to_remove in elements_to_remove:
91 del elements[ele_guid_to_remove]
93 @staticmethod
94 def add_bounds_to_elements(
95 elements: dict, space_boundaries: dict[str, SpaceBoundary]):
96 """Add space boundaries to elements.
98 This function adds those space boundaries from space_boundaries to
99 elements. This includes all space boundaries included in
100 space_boundaries, which bound an IfcSpace. The space boundaries which
101 have been excluded during the preprocessing in the kernel are skipped
102 by only considering boundaries from the space_boundaries dictionary.
104 Args:
105 elements: dict[guid: element]
106 space_boundaries: dict[guid: SpaceBoundary]
107 """
108 logger.info("Creates python representation of relevant ifc types")
109 instance_dict = {}
110 spaces = get_spaces_with_bounds(elements)
111 total_bounds_removed = 0
112 for space in spaces:
113 drop_bound_counter = 0
114 keep_bounds = []
115 for bound in space.space_boundaries:
116 if not bound.guid in space_boundaries.keys():
117 drop_bound_counter += 1
118 continue
119 else:
120 instance_dict[bound.guid] = bound
121 keep_bounds.append(bound)
122 total_bounds_removed += drop_bound_counter
123 space.space_boundaries = keep_bounds
124 if drop_bound_counter > 0:
125 logger.info(f"Removed {drop_bound_counter} space boundaries in "
126 f"{space.guid} {space.name}")
127 if total_bounds_removed > 0:
128 logger.warning(f"Total of {total_bounds_removed} space boundaries "
129 f"removed.")
130 elements.update(instance_dict)
132 def get_parents_and_children(self, sim_settings: BaseSimSettings,
133 boundaries: list[SpaceBoundary],
134 elements: dict, opening_area_tolerance=0.01) \
135 -> dict[str, SpaceBoundary]:
136 """Get parent-children relationships between space boundaries.
138 This function computes the parent-children relationships between
139 IfcElements (e.g. Windows, Walls) to obtain the corresponding
140 relationships of their space boundaries.
142 Args:
143 sim_settings: BIM2SIM EnergyPlus simulation settings
144 boundaries: list of SpaceBoundary elements
145 elements: dict[guid: element]
146 opening_area_tolerance: Tolerance for comparison of opening areas.
147 Returns:
148 bound_dict: dict[guid: element]
149 """
150 logger.info("Compute relationships between space boundaries")
151 logger.info("Compute relationships between openings and their "
152 "base surfaces")
153 drop_list = {} # HACK: dictionary for bounds which have to be removed
154 bound_dict = {bound.guid: bound for bound in boundaries}
155 temp_elements = elements.copy()
156 temp_elements.update(bound_dict)
157 # from elements (due to duplications)
158 for inst_obj in boundaries:
159 if inst_obj.level_description == "2b":
160 continue
161 inst_obj_space = inst_obj.ifc.RelatingSpace
162 b_inst = inst_obj.bound_element
163 if b_inst is None:
164 continue
165 # assign opening elems (Windows, Doors) to parents and vice versa
166 related_opening_elems = \
167 self.get_related_opening_elems(b_inst, temp_elements)
168 if not related_opening_elems:
169 continue
170 # assign space boundaries of opening elems (Windows, Doors)
171 # to parents and vice versa
172 for opening in related_opening_elems:
173 op_bound = self.get_opening_boundary(
174 inst_obj, inst_obj_space, opening,
175 sim_settings.max_wall_thickness)
176 if not op_bound:
177 continue
178 # HACK:
179 # find cases where opening area matches area of corresponding
180 # wall (within inner loop) and reassign the current opening
181 # boundary to the surrounding boundary (which is the true
182 # parent boundary)
183 if (inst_obj.bound_area - op_bound.bound_area).m \
184 < opening_area_tolerance:
185 rel_bound, drop_list = self.reassign_opening_bounds(
186 inst_obj, op_bound, b_inst, drop_list,
187 sim_settings.max_wall_thickness)
188 if not rel_bound:
189 continue
190 rel_bound.opening_bounds.append(op_bound)
191 op_bound.parent_bound = rel_bound
192 else:
193 inst_obj.opening_bounds.append(op_bound)
194 op_bound.parent_bound = inst_obj
195 # remove boundaries from dictionary if they are false duplicates of
196 # windows in shape of walls
197 bound_dict = {k: v for k, v in bound_dict.items() if k not in drop_list}
198 return bound_dict
200 @staticmethod
201 def get_related_opening_elems(bound_element: Element, elements: dict) \
202 -> list[Union[Window, Door]]:
203 """Get related opening elements of current building element.
205 This function returns all opening elements of the current related
206 building element which is related to the current space boundary.
208 Args:
209 bound_element: BIM2SIM building element (e.g., Wall, Floor, ...)
210 elements: dict[guid: element]
211 Returns:
212 related_opening_elems: list of Window and Door elements
213 """
214 related_opening_elems = []
215 if not hasattr(bound_element.ifc, 'HasOpenings'):
216 return related_opening_elems
217 if len(bound_element.ifc.HasOpenings) == 0:
218 return related_opening_elems
220 for opening in bound_element.ifc.HasOpenings:
221 if hasattr(opening.RelatedOpeningElement, 'HasFillings'):
222 for fill in opening.RelatedOpeningElement.HasFillings:
223 opening_obj = elements[
224 fill.RelatedBuildingElement.GlobalId]
225 related_opening_elems.append(opening_obj)
226 return related_opening_elems
228 @staticmethod
229 def get_opening_boundary(this_boundary: SpaceBoundary,
230 this_space: ThermalZone,
231 opening_elem: Union[Window, Door],
232 max_wall_thickness=0.3) \
233 -> Union[SpaceBoundary, None]:
234 """Get related opening boundary of another space boundary.
236 This function returns the related opening boundary of another
237 space boundary.
239 Args:
240 this_boundary: current element of SpaceBoundary
241 this_space: ThermalZone element
242 opening_elem: BIM2SIM element of Window or Door.
243 max_wall_thickness: maximum expected wall thickness in the building.
244 Space boundaries of openings may be displaced by this distance.
245 Returns:
246 opening_boundary: Union[SpaceBoundary, None]
247 """
248 opening_boundary: Union[SpaceBoundary, None] = None
249 distances = {}
250 for op_bound in opening_elem.space_boundaries:
251 if not op_bound.ifc.RelatingSpace == this_space:
252 continue
253 if op_bound in this_boundary.opening_bounds:
254 continue
255 center_shape = BRepBuilderAPI_MakeVertex(
256 gp_Pnt(op_bound.bound_center)).Shape()
257 center_dist = BRepExtrema_DistShapeShape(
258 this_boundary.bound_shape,
259 center_shape,
260 Extrema_ExtFlag_MIN
261 ).Value()
262 if center_dist > max_wall_thickness:
263 continue
264 distances[center_dist] = op_bound
265 sorted_distances = dict(sorted(distances.items()))
266 if sorted_distances:
267 opening_boundary = next(iter(sorted_distances.values()))
268 return opening_boundary
270 @staticmethod
271 def reassign_opening_bounds(this_boundary: SpaceBoundary,
272 opening_boundary: SpaceBoundary,
273 bound_element: Element,
274 drop_list: dict[str, SpaceBoundary],
275 max_wall_thickness=0.3,
276 angle_tolerance=0.1) -> \
277 tuple[SpaceBoundary, dict[str, SpaceBoundary]]:
278 """Fix assignment of parent and child space boundaries.
280 This function reassigns the current opening bound as an opening
281 boundary of its surrounding boundary. This function only applies if
282 the opening boundary has the same surface area as the assigned parent
283 surface.
284 HACK:
285 Some space boundaries have inner loops which are removed for vertical
286 bounds in calc_bound_shape (elements.py). Those inner loops contain
287 an additional vertical bound (wall) which is "parent" of an
288 opening. EnergyPlus does not accept openings having a parent
289 surface of same size as the opening. Thus, since inner loops are
290 removed from shapes beforehand, those boundaries are removed from
291 "elements" and the openings are assigned to have the larger
292 boundary as a parent.
294 Args:
295 this_boundary: current element of SpaceBoundary
296 opening_boundary: current element of opening SpaceBoundary (
297 related to BIM2SIM Window or Door)
298 bound_element: BIM2SIM building element (e.g., Wall, Floor, ...)
299 drop_list: dict[str, SpaceBoundary] with SpaceBoundary elements
300 that have same size as opening space boundaries and therefore
301 should be dropped
302 max_wall_thickness: maximum expected wall thickness in the building.
303 Space boundaries of openings may be displaced by this distance.
304 angle_tolerance: tolerance for comparison of surface normal angles.
305 Returns:
306 rel_bound: New parent boundary for the opening that had the same
307 geometry as its previous parent boundary
308 drop_list: Updated dict[str, SpaceBoundary] with SpaceBoundary
309 elements that have same size as opening space boundaries and
310 therefore should be dropped
311 """
312 rel_bound = None
313 drop_list[this_boundary.guid]: dict[str, SpaceBoundary] = this_boundary
314 ib = [b for b in bound_element.space_boundaries if
315 b.ifc.ConnectionGeometry.SurfaceOnRelatingElement.InnerBoundaries
316 if
317 b.bound_thermal_zone == opening_boundary.bound_thermal_zone]
318 if len(ib) == 1:
319 rel_bound = ib[0]
320 elif len(ib) > 1:
321 for b in ib:
322 # check if orientation of possibly related bound is the same
323 # as opening
324 angle = math.degrees(
325 gp_Dir(b.bound_normal).Angle(gp_Dir(
326 opening_boundary.bound_normal)))
327 if not (angle < 0 + angle_tolerance
328 or angle > 180 - angle_tolerance):
329 continue
330 distance = BRepExtrema_DistShapeShape(
331 b.bound_shape,
332 opening_boundary.bound_shape,
333 Extrema_ExtFlag_MIN
334 ).Value()
335 if distance > max_wall_thickness:
336 continue
337 else:
338 rel_bound = b
339 else:
340 tzb = \
341 [b for b in
342 opening_boundary.bound_thermal_zone.space_boundaries if
343 b.ifc.ConnectionGeometry.SurfaceOnRelatingElement.InnerBoundaries]
344 for b in tzb:
345 # check if orientation of possibly related bound is the same
346 # as opening
347 angle = None
348 try:
349 angle = math.degrees(
350 gp_Dir(b.bound_normal).Angle(
351 gp_Dir(opening_boundary.bound_normal)))
352 except Exception as ex:
353 logger.warning(f"Unexpected {ex=}. Comparison of bound "
354 f"normals failed for "
355 f"{b.guid} and {opening_boundary.guid}. "
356 f"{type(ex)=}")
357 if not (angle < 0 + angle_tolerance
358 or angle > 180 - angle_tolerance):
359 continue
360 distance = BRepExtrema_DistShapeShape(
361 b.bound_shape,
362 opening_boundary.bound_shape,
363 Extrema_ExtFlag_MIN
364 ).Value()
365 if distance > max_wall_thickness:
366 continue
367 else:
368 rel_bound = b
369 return rel_bound, drop_list
371 def instantiate_space_boundaries(
372 self, entities_dict: dict, elements: dict, finder:
373 TemplateFinder,
374 create_external_elements: bool, ifc_units: dict[str, ureg]) \
375 -> List[RelationBased]:
376 """Instantiate space boundary ifc_entities.
378 This function instantiates space boundaries using given element class.
379 Result is a list with the resulting valid elements.
381 Args:
382 entities_dict: dict of Ifc Entities (as str)
383 elements: dict[guid: element]
384 finder: BIM2SIM TemplateFinder
385 create_external_elements: bool, True if external spatial elements
386 should be considered for space boundary setup
387 ifc_units: dict of IfcMeasures and Unit (ureg)
388 Returns:
389 list of dict[guid: SpaceBoundary]
390 """
391 element_lst = {}
392 for entity in entities_dict:
393 if entity.is_a() == 'IfcRelSpaceBoundary1stLevel' or \
394 entity.Name == '1stLevel':
395 continue
396 if entity.RelatingSpace.is_a('IfcSpace'):
397 element = SpaceBoundary.from_ifc(
398 entity, elements=element_lst, finder=finder,
399 ifc_units=ifc_units)
400 elif create_external_elements and entity.RelatingSpace.is_a(
401 'IfcExternalSpatialElement'):
402 element = ExtSpatialSpaceBoundary.from_ifc(
403 entity, elements=element_lst, finder=finder,
404 ifc_units=ifc_units)
405 else:
406 continue
407 # for RelatingSpaces both IfcSpace and IfcExternalSpatialElement are
408 # considered
409 relating_space = elements.get(
410 element.ifc.RelatingSpace.GlobalId, None)
411 if relating_space is not None:
412 self.connect_space_boundaries(element, relating_space,
413 elements)
414 element_lst[element.guid] = element
416 return list(element_lst.values())
418 def connect_space_boundaries(
419 self, space_boundary: SpaceBoundary, relating_space: ThermalZone,
420 elements: dict[str, IFCBased]):
421 """Connect space boundary with relating space.
423 Connects resulting space boundary with the corresponding relating
424 space (i.e., ThermalZone) and related building element (if given).
426 Args:
427 space_boundary: SpaceBoundary
428 relating_space: ThermalZone (relating space)
429 elements: dict[guid: element]
430 """
431 relating_space.space_boundaries.append(space_boundary)
432 space_boundary.bound_thermal_zone = relating_space
434 if space_boundary.ifc.RelatedBuildingElement:
435 related_building_element = elements.get(
436 space_boundary.ifc.RelatedBuildingElement.GlobalId, None)
437 if related_building_element:
438 related_building_element.space_boundaries.append(space_boundary)
439 space_boundary.bound_element = related_building_element
440 self.connect_element_to_zone(relating_space,
441 related_building_element)
443 @staticmethod
444 def connect_element_to_zone(thermal_zone: ThermalZone,
445 bound_element: IFCBased):
446 """Connects related building element and corresponding thermal zone.
448 This function connects a thermal zone and its IFCBased related
449 building elements.
451 Args:
452 thermal_zone: ThermalZone
453 bound_element: BIM2SIM IFCBased element
454 """
455 if bound_element not in thermal_zone.bound_elements:
456 thermal_zone.bound_elements.append(bound_element)
457 if thermal_zone not in bound_element.thermal_zones:
458 bound_element.thermal_zones.append(thermal_zone)