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

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING, Union, Type, Any 

4 

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 

16 

17if TYPE_CHECKING: 

18 from bim2sim.elements.bps_elements import SpaceBoundary 

19 

20 

21class DisaggregationCreationAndTypeCheck(ITask): 

22 """Disaggregation of elements, run() method holds detailed information.""" 

23 

24 reads = ('elements',) 

25 

26 def run(self, elements: dict): 

27 """Disaggregates building elements based on their space boundaries. 

28 

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. 

41 

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. 

45 

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) 

136 

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 

142 

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] 

148 

149 def type_correction_not_disaggregation( 

150 self, element: BPSProductWithLayers, sbs: list['SpaceBoundary']): 

151 """Performs type correction for non disaggregated elements. 

152 

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 

185 

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. 

189 

190 Args: 

191 element (BPSProductWithLayers): The element to disaggregate. 

192 sbs (list[SpaceBoundary]): List of space boundaries associated with 

193 the element. 

194 

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 

260 

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. 

264 

265 Args: 

266 element (BPSProductWithLayers): The element to check. 

267 sbs (list[SpaceBoundary]): List of space boundaries associated with 

268 the element. 

269 

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 

289 

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. 

294 

295 Args: 

296 element (BPSProductWithLayers): The element to check. 

297 sbs (list[SpaceBoundary]): List of space boundaries associated with 

298 the element. 

299 

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 

319 

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. 

325 

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