Coverage for bim2sim/tasks/bps/disaggr_creation.py: 11%
154 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
3from typing import TYPE_CHECKING, Union, Type, Any
5from bim2sim.elements.aggregation.bps_aggregations import \
6 InnerWallDisaggregated, OuterWallDisaggregated, GroundFloorDisaggregated, \
7 RoofDisaggregated, InnerFloorDisaggregated, InnerDoorDisaggregated, \
8 OuterDoorDisaggregated
9from bim2sim.elements.bps_elements import (
10 Slab, Wall, InnerWall, OuterWall, GroundFloor, Roof, InnerFloor,
11 BPSProductWithLayers, InnerDoor, OuterDoor, Door, ExtSpatialSpaceBoundary)
12from bim2sim.elements.mapping.units import ureg
13from bim2sim.tasks.base import ITask
14from bim2sim.utilities.common_functions import all_subclasses
15from bim2sim.utilities.types import BoundaryOrientation
17if TYPE_CHECKING:
18 from bim2sim.elements.bps_elements import SpaceBoundary
21class DisaggregationCreationAndTypeCheck(ITask):
22 """Disaggregation of elements, run() method holds detailed information."""
24 reads = ('elements',)
26 def run(self, elements: dict):
27 """Disaggregates building elements based on their space boundaries.
29 This task disaggregates the building elements like walls, slabs etc.
30 based on their SpaceBoundaries. This is needed for two reasons:
31 1. If e.g. a BaseSlab in IFC is modeled as one element for whole
32 building but only parts of this BaseSlab have contact to ground,
33 we can
34 split the BaseSlab based on the space boundary information into
35 single parts that hold the correct boundary conditions and material
36 layer information in the later simulation.
37 2. In TEASER we use CombineThermalZones Task to combine multiple
38 ThermalZone elements into AggregatedThermalZones to improve simulation
39 speed and accuracy. For this we need to split all elements into the
40 parts that belong to each ThermalZone.
42 This Task also checks and corrects the type of the non disaggregated
43 elements based on their SpaceBoundary information, because sometimes
44 the predefined types in IFC might not be correct.
46 Args:
47 elements (dict): Dictionary of building elements to process.
48 """
49 elements_overwrite = {}
50 elements_to_aggregate = {} # dict(new_element, old_element)
51 for ele in elements.values():
52 # only handle BPSProductWithLayers
53 if not any([isinstance(ele, bps_product_layer_ele) for
54 bps_product_layer_ele in
55 all_subclasses(BPSProductWithLayers)]):
56 continue
57 # no disaggregation needed
58 if len(ele.space_boundaries) < 2:
59 self.logger.info(f'No disggregation needed for {ele}')
60 continue
61 disaggregations = []
62 for sb in ele.space_boundaries:
63 disaggr = None
64 # the space_boundaries may contain those space boundaries,
65 # which do not have an IfcSpace as RelatingSpace, but an
66 # ExternalSpatialElement. These are handeled in bim2sim as
67 # ExternalSpatialSpaceBoundaries and should be excluded for
68 # disaggregation.
69 if isinstance(sb, ExtSpatialSpaceBoundary):
70 continue
71 # skip if disaggregation already exists for this SB
72 if sb.disagg_parent:
73 continue
74 if sb.related_bound:
75 # todo
76 # related_bounds may have different bound_elements if,
77 # e.g., floor and ceiling are modeled as individual slabs
78 # that are directly bounded without air gap. These should
79 # be aggregated in further development, but for now,
80 # these are aggregated in this disaggregation process
81 if ele != sb.related_bound.bound_element:
82 elements_to_aggregate.update({
83 ele: sb.related_bound.bound_element})
84 # sb with related bound and only 2 sbs needs no
85 # disaggregation
86 if len(ele.space_boundaries) == 2:
87 self.logger.info(f'No disggregation needed for {ele}')
88 continue
89 if len(ele.space_boundaries) > 2:
90 # as above: if the related_bound of a space boundary
91 # is an ExternalSpatialSpaceBoundary,
92 # this related_bound should not be considered for
93 # disaggregation, and the space boundary should be
94 # treated as it had no partner in an adjacent space.
95 if isinstance(sb.related_bound,
96 ExtSpatialSpaceBoundary):
97 disaggr = (
98 self.
99 create_disaggregation_with_type_correction(
100 ele, [sb]))
101 else:
102 disaggr = (
103 self.
104 create_disaggregation_with_type_correction(
105 ele, [sb, sb.related_bound]))
106 else:
107 self.logger.info(f'No disggregation needed for {ele}')
108 else:
109 disaggr = self.create_disaggregation_with_type_correction(
110 ele, [sb])
111 if disaggr:
112 disaggregations.append(disaggr)
113 if disaggregations:
114 elements_overwrite[ele] = disaggregations
115 # check if disaggregations are complete
116 area_disaggr = 0
117 for disaggr in disaggregations:
118 area_disaggr += disaggr.gross_area
119 diff = (abs(ele.gross_area - area_disaggr) /
120 ele.gross_area * 100) * ureg.percent
121 if diff > 5 * ureg.percent:
122 self.logger.warning(
123 f"Found a difference of {round(diff,2)} area of created"
124 f" Disaggregations and original element "
125 f"{ele}, with GUID {ele.guid}. Please check this.")
126 else:
127 # this type check should only be performed for elements that
128 # hold common SpaceBoundary entities, but not for those,
129 # which only have ExternalSpatialSpaceBoundaries.
130 type_check_sbs = \
131 [s for s in ele.space_boundaries if not
132 isinstance(s, ExtSpatialSpaceBoundary)]
133 if len(type_check_sbs) > 0:
134 self.type_correction_not_disaggregation(
135 ele, type_check_sbs)
137 # add disaggregations and remove their parent from elements
138 for ele, replacements in elements_overwrite.items():
139 del elements[ele.guid]
140 for replace in replacements:
141 elements[replace.guid] = replace
143 # remove elements to aggregate (values only, these are deprecated)
144 # from elements dictionary.
145 for key, value in elements_to_aggregate.items():
146 if value in elements:
147 del elements[value.guid]
149 def type_correction_not_disaggregation(
150 self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']):
151 """Performs type correction for non disaggregated elements.
153 Args:
154 element (BPSProductWithLayers): The element to correct.
155 sbs (list[SpaceBoundary]): List of space boundaries associated with
156 the element.
157 """
158 wall_type = self.get_corrected_wall_type(element, sbs)
159 if wall_type:
160 if not isinstance(element, wall_type):
161 self.logger.info(f'Replacing {element.__class__.__name__} '
162 f'with {wall_type.__name__} for '
163 f'element with IFC GUID {element.guid} based '
164 f'on SB information.')
165 element.__class__ = wall_type
166 return
167 slab_type = self.get_corrected_slab_type(element, sbs)
168 if slab_type:
169 if not isinstance(element, slab_type):
170 self.logger.info(f'Replacing {element.__class__.__name__} '
171 f'with {slab_type.__name__} for '
172 f'element with IFC GUID {element.guid} based '
173 f'on SB information.')
174 element.__class__ = slab_type
175 return
176 door_type = self.get_corrected_door_type(element, sbs)
177 if door_type:
178 if not isinstance(element, door_type):
179 self.logger.info(f'Replacing {element.__class__.__name__} '
180 f'with {door_type.__name__} for '
181 f'element with IFC GUID {element.guid} based '
182 f'on SB information.')
183 element.__class__ = door_type
184 return
186 def create_disaggregation_with_type_correction(
187 self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']) -> BPSProductWithLayers:
188 """Creates a disaggregation for an element including type correction.
190 Args:
191 element (BPSProductWithLayers): The element to disaggregate.
192 sbs (list[SpaceBoundary]): List of space boundaries associated with
193 the element.
195 Returns:
196 BPSProductWithLayers: The disaggregated element with the correct
197 type.
198 """
199 disaggr = None
200 # if Wall
201 wall_type = self.get_corrected_wall_type(element, sbs)
202 if wall_type:
203 if wall_type == InnerWall:
204 disaggr = InnerWallDisaggregated(
205 element, sbs)
206 elif wall_type == OuterWall:
207 disaggr = OuterWallDisaggregated(
208 element, sbs)
209 if disaggr:
210 if not isinstance(element, wall_type):
211 self.logger.info(f'Replacing {element.__class__.__name__} '
212 f'with {wall_type.__name__} for'
213 f' disaggregated element with parent IFC'
214 f' GUID {element.guid} based on SB'
215 f' information.')
216 return disaggr
217 # if Slab
218 slab_type = self.get_corrected_slab_type(element, sbs)
219 if slab_type:
220 if slab_type == GroundFloor:
221 disaggr = GroundFloorDisaggregated(
222 element, sbs
223 )
224 elif slab_type == Roof:
225 disaggr = RoofDisaggregated(
226 element, sbs
227 )
228 elif slab_type == InnerFloor:
229 disaggr = InnerFloorDisaggregated(
230 element, sbs
231 )
232 elif slab_type == OuterWall:
233 disaggr = OuterWallDisaggregated(
234 element, sbs
235 )
236 if disaggr:
237 if not isinstance(element, slab_type):
238 self.logger.info(f'Replacing {element.__class__.__name__} '
239 f'with {slab_type.__name__} for'
240 f' disaggregated element with parent IFC'
241 f' GUID {element.guid} based on SB'
242 f' information.')
243 return disaggr
244 door_type = self.get_corrected_door_type(element, sbs)
245 if door_type:
246 if door_type == InnerDoor:
247 disaggr = InnerDoorDisaggregated(
248 element, sbs)
249 elif door_type == OuterDoor:
250 disaggr = OuterDoorDisaggregated(
251 element, sbs)
252 if disaggr:
253 if not isinstance(element, door_type):
254 self.logger.info(f'Replacing {element.__class__.__name__} '
255 f'with {door_type.__name__} for'
256 f' disaggregated element with parent IFC'
257 f' GUID {element.guid} based on SB'
258 f' information.')
259 return disaggr
261 def get_corrected_door_type(self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']) -> (
262 Type[InnerDoor] | Type[OuterDoor] | None):
263 """Gets the correct door type based on space boundary information.
265 Args:
266 element (BPSProductWithLayers): The element to check.
267 sbs (list[SpaceBoundary]): List of space boundaries associated with
268 the element.
270 Returns:
271 type: The correct door type or None if not applicable.
272 """
273 if any([isinstance(element, door_class) for door_class in
274 all_subclasses(Door)]):
275 # Corresponding Boundaries
276 if len(sbs) == 2:
277 return InnerDoor
278 elif len(sbs) == 1:
279 # external Boundary
280 if sbs[0].is_external:
281 return OuterDoor
282 # 2B space Boundary
283 else:
284 return InnerDoor
285 else:
286 return self.logger("Error in check of correct door type")
287 else:
288 return None
290 def get_corrected_wall_type(
291 self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']) -> (
292 Type[InnerWall] | Type[OuterWall] | None):
293 """Gets the correct wall type based on space boundary information.
295 Args:
296 element (BPSProductWithLayers): The element to check.
297 sbs (list[SpaceBoundary]): List of space boundaries associated with
298 the element.
300 Returns:
301 type: The correct wall type or None if not applicable.
302 """
303 if any([isinstance(element, wall_class) for wall_class in
304 all_subclasses(Wall)]):
305 # Corresponding Boundaries
306 if len(sbs) == 2:
307 return InnerWall
308 elif len(sbs) == 1:
309 # external Boundary
310 if sbs[0].is_external:
311 return OuterWall
312 # 2B space Boundary
313 else:
314 return InnerWall
315 else:
316 return self.logger("Error in check of correct wall type")
317 else:
318 return None
320 def get_corrected_slab_type(
321 self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']) -> (
322 Type[InnerFloor] | Type[GroundFloor] | None | Type[Roof],
323 Type[OuterWall]):
324 """Gets the correct slab type based on space boundary information.
326 Args:
327 element (BPSProductWithLayers): The element to check.
328 sbs (list[SpaceBoundary]): List of space boundaries associated with
329 the element
330 Returns:
331 type: The correct wall type or None if not applicable.
332 """
333 if any([isinstance(element, slab_class) for slab_class in
334 all_subclasses(Slab)]):
335 # Corresponding Boundaries
336 if len(sbs) == 2:
337 return InnerFloor
338 elif len(sbs) == 1:
339 # external Boundary
340 sb = sbs[0]
341 if sb.is_external:
342 if sb.internal_external_type == 'EXTERNAL_EARTH':
343 return GroundFloor
344 elif sb.top_bottom == BoundaryOrientation.bottom:
345 # Possible failure for overhangs that are external but
346 # have contact to air, because IFC provides
347 # information about "EXTERNAL_EARTH" only in rare cases
348 return GroundFloor
349 elif sb.top_bottom == BoundaryOrientation.top:
350 return Roof
351 # vertical slabs might occur in IFC but will be mapped to
352 # bim2sim OuterWall
353 elif sb.top_bottom == BoundaryOrientation.vertical:
354 return OuterWall
355 else:
356 self.logger.error(f"Error in type correction of "
357 f"{element}")
358 # 2B space Boundary
359 else:
360 return InnerFloor
361 else:
362 return self.logger.error("Error in check of correct wall type")
363 else:
364 return None