Coverage for bim2sim/tasks/bps/sb_correction.py: 14%

251 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

1"""Geometric Correction of Space Boundaries. 

2 

3This module contains all functions for geometric preprocessing of the BIM2SIM 

4Elements that are relevant for exporting EnergyPlus Input Files and other BPS 

5applications. Geometric preprocessing mainly relies on shape 

6manipulations with OpenCascade (OCC). This module is prerequisite for the 

7BIM2SIM PluginEnergyPlus. This module must be executed before exporting the 

8EnergyPlus Input file. 

9""" 

10import copy 

11import logging 

12from typing import Union 

13 

14from ifcopenshell import guid 

15from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform, \ 

16 BRepBuilderAPI_Sewing 

17from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape 

18from OCC.Core.BRepGProp import brepgprop_VolumeProperties, \ 

19 brepgprop_SurfaceProperties 

20from OCC.Core.Extrema import Extrema_ExtFlag_MIN 

21from OCC.Core.GProp import GProp_GProps 

22from OCC.Core.TopAbs import TopAbs_FACE 

23from OCC.Core.TopExp import TopExp_Explorer 

24from OCC.Core.TopoDS import topods_Face, TopoDS_Shape 

25from OCC.Core.gp import gp_Pnt, gp_Trsf, gp_XYZ, gp_Vec 

26 

27from bim2sim.elements.bps_elements import ExternalSpatialElement, \ 

28 SpaceBoundary, \ 

29 SpaceBoundary2B, ExtSpatialSpaceBoundary 

30from bim2sim.tasks.base import ITask 

31from bim2sim.tasks.common.inner_loop_remover import convex_decomposition, \ 

32 is_convex_no_holes, is_convex_slow 

33from bim2sim.utilities.common_functions import filter_elements, \ 

34 get_spaces_with_bounds 

35from bim2sim.utilities.pyocc_tools import PyOCCTools 

36from bim2sim.tasks.base import Playground 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class CorrectSpaceBoundaries(ITask): 

42 """Advanced geometric preprocessing for Space Boundaries. 

43 

44 This class includes all functions for advanced geometric preprocessing 

45 required for high level space boundary handling, e.g., required by 

46 EnergyPlus export. See detailed explanation in the run 

47 function below. 

48 """ 

49 reads = ('elements',) 

50 

51 def __init__(self, playground: Playground): 

52 super().__init__(playground) 

53 

54 def run(self, elements: dict): 

55 """Geometric preprocessing for BPS. 

56 

57 This module contains all functions for geometric preprocessing of the BIM2SIM 

58 Elements that are relevant for exporting BPS Input Files within the 

59 Plugins EnergyPlus, Comfort and TEASER. This geometric preprocessing mainly  

60 relies on shape manipulations with OpenCascade (OCC).  

61 This task starts with linking the space boundaries to the dictionary 

62 of elements. Additionally, geometric preprocessing operations are 

63 executed, like moving opening elements to their parent surfaces ( 

64 unless they are already coplanar), the surface orientation of space 

65 boundaries are fixed, and non-convex boundaries are fixed. 

66 

67 Args: 

68 elements (dict): dictionary in the format dict[guid: element], 

69 Dictionary of elements generated in previous IFC-based setup and 

70 enrichment tasks. In this task, the elements are enriched with 

71 the geometric preprocessed space_boundary items. 

72 space_boundaries (dict): dictionary in the format dict[guid: 

73 SpaceBoundary], dictionary of IFC-based space boundary elements. 

74 """ 

75 if not self.playground.sim_settings.correct_space_boundaries: 

76 return 

77 logger.info("Geometric correction of space boundaries started...") 

78 # todo: refactor elements to initial_elements. 

79 # todo: space_boundaries should be already included in elements 

80 self.move_children_to_parents(elements) 

81 self.fix_surface_orientation(elements) 

82 self.split_non_convex_bounds( 

83 elements, self.playground.sim_settings.split_bounds) 

84 self.add_and_split_bounds_for_shadings( 

85 elements, self.playground.sim_settings.add_shadings, 

86 self.playground.sim_settings.split_shadings) 

87 logger.info("Geometric correction of space boundaries finished!") 

88 

89 def add_and_split_bounds_for_shadings(self, elements: dict, 

90 add_shadings: bool, 

91 split_shadings: bool): 

92 """Add and split shading boundaries. 

93 

94 Enrich elements by space boundaries related to an 

95 ExternalSpatialElement if shadings are to be added in the energyplus 

96 workflow. 

97 

98 Args: 

99 elements: dict[guid: element] 

100 add_shadings: True if shadings shall be added 

101 split_shadings: True if shading boundaries should be split in 

102 non-convex boundaries 

103 """ 

104 if add_shadings: 

105 spatials = [] 

106 ext_spatial_elems = filter_elements(elements, 

107 ExternalSpatialElement) 

108 for elem in ext_spatial_elems: 

109 for sb in elem.space_boundaries: 

110 spatials.append(sb) 

111 if spatials and split_shadings: 

112 self.split_non_convex_shadings(elements, spatials) 

113 

114 @staticmethod 

115 def move_children_to_parents(elements: dict): 

116 """Move child space boundaries to parent boundaries. 

117 

118 In some IFC, the opening boundaries of external wall 

119 boundaries are not coplanar. This function moves external opening 

120 boundaries to related parent boundary (e.g. wall). 

121 

122 Args: 

123 elements: dict[guid: element] 

124 """ 

125 logger.info("Move openings to base surface, if needed") 

126 boundaries = filter_elements(elements, SpaceBoundary) 

127 for bound in boundaries: 

128 if bound.parent_bound: 

129 opening_obj = bound 

130 # only external openings need to be moved 

131 # all other are properly placed within parent boundary 

132 if opening_obj.is_external: 

133 distance = BRepExtrema_DistShapeShape( 

134 opening_obj.bound_shape, 

135 opening_obj.parent_bound.bound_shape, 

136 Extrema_ExtFlag_MIN).Value() 

137 if distance < 0.001: 

138 continue 

139 prod_vec = [] 

140 for i in opening_obj.bound_normal.Coord(): 

141 prod_vec.append(distance * i) 

142 

143 # moves opening to parent boundary 

144 trsf = gp_Trsf() 

145 coord = gp_XYZ(*prod_vec) 

146 vec = gp_Vec(coord) 

147 trsf.SetTranslation(vec) 

148 

149 opening_obj.bound_shape_org = opening_obj.bound_shape 

150 opening_obj.bound_shape = BRepBuilderAPI_Transform( 

151 opening_obj.bound_shape, trsf).Shape() 

152 

153 # check if opening has been moved to boundary correctly 

154 # and otherwise move again in reversed direction 

155 new_distance = BRepExtrema_DistShapeShape( 

156 opening_obj.bound_shape, 

157 opening_obj.parent_bound.bound_shape, 

158 Extrema_ExtFlag_MIN).Value() 

159 if new_distance > 1e-3: 

160 prod_vec = [] 

161 op_normal = opening_obj.bound_normal.Reversed() 

162 for i in op_normal.Coord(): 

163 prod_vec.append(new_distance * i) 

164 trsf = gp_Trsf() 

165 coord = gp_XYZ(*prod_vec) 

166 vec = gp_Vec(coord) 

167 trsf.SetTranslation(vec) 

168 opening_obj.bound_shape = BRepBuilderAPI_Transform( 

169 opening_obj.bound_shape, trsf).Shape() 

170 opening_obj.reset('bound_center') 

171 

172 @staticmethod 

173 def fix_surface_orientation(elements: dict): 

174 """Fix orientation of space boundaries. 

175 

176 Fix orientation of all surfaces but openings by sewing followed 

177 by disaggregation. Fix orientation of openings afterwards according 

178 to orientation of parent bounds. 

179 

180 Args: 

181 elements: dict[guid: element] 

182 """ 

183 logger.info("Fix surface orientation") 

184 spaces = get_spaces_with_bounds(elements) 

185 for space in spaces: 

186 face_list = [] 

187 for bound in space.space_boundaries: 

188 # get all bounds within a space except openings 

189 if bound.parent_bound: 

190 continue 

191 # append all faces within the space to face_list 

192 face = PyOCCTools.get_face_from_shape(bound.bound_shape) 

193 face_list.append(face) 

194 if not face_list: 

195 continue 

196 # if the space has generated 2B space boundaries, add them to 

197 # face_list 

198 if hasattr(space, 'space_boundaries_2B'): 

199 for bound in space.space_boundaries_2B: 

200 face = PyOCCTools.get_face_from_shape(bound.bound_shape) 

201 face_list.append(face) 

202 # sew all faces within the face_list together 

203 sew = BRepBuilderAPI_Sewing(0.0001) 

204 for fc in face_list: 

205 sew.Add(fc) 

206 sew.Perform() 

207 sewed_shape = sew.SewedShape() 

208 fixed_shape = sewed_shape 

209 # check volume of the sewed shape. If negative, not all the 

210 # surfaces have the same orientation 

211 p = GProp_GProps() 

212 brepgprop_VolumeProperties(fixed_shape, p) 

213 if p.Mass() < 0: 

214 # complements the surface orientation within the fixed shape 

215 fixed_shape.Complement() 

216 # disaggregate the fixed_shape to a list of fixed_faces 

217 f_exp = TopExp_Explorer(fixed_shape, TopAbs_FACE) 

218 fixed_faces = [] 

219 while f_exp.More(): 

220 fixed_faces.append(topods_Face(f_exp.Current())) 

221 f_exp.Next() 

222 for fc in fixed_faces: 

223 # compute the surface normal for each face 

224 face_normal = PyOCCTools.simple_face_normal( 

225 fc, check_orientation=False) 

226 # compute the center of mass for the current face 

227 p = GProp_GProps() 

228 brepgprop_SurfaceProperties(fc, p) 

229 face_center = p.CentreOfMass().XYZ() 

230 complemented = False 

231 for bound in space.space_boundaries: 

232 # find the original bound by evaluating the distance of 

233 # the face centers. Continue if the distance is greater 

234 # than the tolerance. 

235 if (gp_Pnt(bound.bound_center).Distance( 

236 gp_Pnt(face_center)) > 1e-3): 

237 continue 

238 # check if the surfaces have the same surface area 

239 if (bound.bound_area.m - p.Mass()) ** 2 < 0.01: 

240 # complement the surfaces if needed 

241 if fc.Orientation() == 1: 

242 bound.bound_shape.Complement() 

243 complemented = True 

244 elif face_normal.Dot(bound.bound_normal) < 0: 

245 bound.bound_shape.Complement() 

246 complemented = True 

247 if not complemented: 

248 continue 

249 # complement openings if parent holds openings 

250 if bound.opening_bounds: 

251 op_bounds = bound.opening_bounds 

252 for op in op_bounds: 

253 op.bound_shape.Complement() 

254 break 

255 if not hasattr(space, 'space_boundaries_2B'): 

256 continue 

257 # if the current face is a generated 2b bound, just keep the 

258 # current face and delete the bound normal property, so it is 

259 # recomputed the next time it is accessed. 

260 for bound in space.space_boundaries_2B: 

261 if gp_Pnt(bound.bound_center).Distance( 

262 gp_Pnt(face_center)) < 1e-6: 

263 bound.bound_shape = fc 

264 if hasattr(bound, 'bound_normal'): 

265 bound.reset('bound_normal') 

266 break 

267 

268 def split_non_convex_bounds(self, elements: dict, split_bounds: bool): 

269 """Split non-convex space boundaries. 

270 

271 This function splits non-convex shapes of space boundaries into 

272 convex shapes. Convex shapes may be required for shading calculations 

273 in Energyplus. 

274 

275 Args: 

276 elements: dict[guid: element] 

277 split_bounds: True if non-convex space boundaries should be split up 

278 into convex shapes. 

279 """ 

280 if not split_bounds: 

281 return 

282 logger.info("Split non-convex surfaces") 

283 # filter elements for type SpaceBoundary 

284 bounds = filter_elements(elements, SpaceBoundary) 

285 if not bounds: 

286 # if no elements of type SpaceBoundary are found, this function 

287 # is applied on SpaceBoundary2B 

288 bounds = filter_elements(elements, SpaceBoundary2B) 

289 # filter for boundaries, that are not opening boundaries 

290 bounds_except_openings = [b for b in bounds if not b.parent_bound] 

291 conv = [] # list of new convex shapes (for debugging) 

292 non_conv = [] # list of old non-convex shapes (for debugging) 

293 for bound in bounds_except_openings: 

294 try: 

295 # check if bound has already been processed 

296 if hasattr(bound, 'convex_processed'): 

297 continue 

298 # check if bound is convex 

299 if is_convex_no_holes(bound.bound_shape): 

300 continue 

301 # check all space boundaries that 

302 # are not parent to an opening bound 

303 if bound.opening_bounds: 

304 if is_convex_slow(bound.bound_shape): 

305 continue 

306 # handle shapes that contain opening bounds 

307 # the surface area of an opening should not be split up 

308 # in the parent face, so for splitting up parent faces, 

309 # the opening boundary must be considered as a non-split 

310 # area 

311 convex_shapes = convex_decomposition(bound.bound_shape, 

312 [op.bound_shape for op 

313 in 

314 bound.opening_bounds]) 

315 else: 

316 # if bound does not have openings, simply compute its 

317 # convex decomposition and returns a list of convex_shapes 

318 convex_shapes = convex_decomposition(bound.bound_shape) 

319 non_conv.append(bound) 

320 if hasattr(bound, 'bound_normal'): 

321 bound.reset('bound_normal') 

322 # create new space boundaries from list of convex shapes, 

323 # for both the bound itself and its corresponding bound (if it 

324 # has 

325 # one) 

326 new_space_boundaries = self.create_new_convex_bounds( 

327 convex_shapes, bound, bound.related_bound) 

328 bound.convex_processed = True 

329 # process related bounds of the processed bounds. For heat 

330 # transfer the corresponding boundaries need to have same 

331 # surface area and same number of vertices, so corresponding 

332 # boundaries must be split up the same way. The split up has 

333 # been taking care of when creating new convex bounds, 

334 # so they only need to be removed here. 

335 if (bound.related_bound and 

336 bound.related_bound.ifc.RelatingSpace.is_a('IfcSpace')) \ 

337 and not bound.ifc.Description == '2b': 

338 non_conv.append(bound.related_bound) 

339 # delete the related bound from elements 

340 del elements[bound.related_bound.guid] 

341 bounds_except_openings.remove(bound.related_bound) 

342 bound.related_bound.convex_processed = True 

343 # delete the current bound from elements 

344 del elements[bound.guid] 

345 # add all new created convex bounds to elements 

346 for new_bound in new_space_boundaries: 

347 elements[new_bound.guid] = new_bound 

348 if bound in new_bound.bound_element.space_boundaries: 

349 new_bound.bound_element.space_boundaries.remove(bound) 

350 new_bound.bound_element.space_boundaries.append(new_bound) 

351 if bound in new_bound.bound_thermal_zone.space_boundaries: 

352 new_bound.bound_thermal_zone.space_boundaries.remove(bound) 

353 new_bound.bound_thermal_zone.space_boundaries.append(new_bound) 

354 conv.append(new_bound) 

355 except Exception as ex: 

356 logger.warning(f"Unexpected {ex}. Converting bound " 

357 f"{bound.guid} to convex shape failed. " 

358 f"{type(ex)}") 

359 

360 @staticmethod 

361 def create_new_boundary( 

362 bound: SpaceBoundary, 

363 shape: TopoDS_Shape) -> SpaceBoundary: 

364 """Create a copy of a SpaceBoundary instance. 

365 

366 This function creates a new space boundary based on the existing one. 

367 

368 Args: 

369 bound: SpaceBoundary 

370 shape: Shape for new boundary 

371 """ 

372 if isinstance(bound, SpaceBoundary2B): 

373 new_bound = SpaceBoundary2B(elements={}) 

374 elif isinstance(bound, ExtSpatialSpaceBoundary): 

375 new_bound = ExtSpatialSpaceBoundary(elements={}) 

376 else: 

377 new_bound = SpaceBoundary(elements={}) 

378 new_bound.guid = guid.new() 

379 new_bound.bound_element = bound.bound_element 

380 new_bound.bound_thermal_zone = bound.bound_thermal_zone 

381 new_bound.ifc = bound.ifc 

382 new_bound.non_convex_guid = bound.non_convex_guid 

383 new_bound.bound_shape = shape 

384 new_bound.related_bound = bound.related_bound 

385 new_bound.related_adb_bound = bound.related_adb_bound 

386 new_bound.reset('is_external') 

387 return new_bound 

388 

389 def create_new_convex_bounds(self, convex_shapes: list[TopoDS_Shape], 

390 bound: Union[SpaceBoundary, SpaceBoundary2B], 

391 related_bound: SpaceBoundary = None): 

392 """Create new convex space boundaries. 

393 

394 This function creates new convex space boundaries from non-convex 

395 space boundary shapes. As for heat transfer the corresponding boundaries 

396 need to have same surface area and same number of vertices, 

397 corresponding boundaries must be split up the same way. Thus, 

398 the bound itself and the corresponding boundary (related_bound) are 

399 treated equally here. 

400 

401 Args: 

402 convex_shapes: List[convex TopoDS_Shape] 

403 bound: either SpaceBoundary or SpaceBoundary2B 

404 related_bound: None or SpaceBoundary (as SpaceBoundary2B do not 

405 have a related_bound) 

406 """ 

407 # keep the original guid as non_convex_guid 

408 bound.non_convex_guid = bound.guid 

409 new_space_boundaries = [] 

410 openings = [] 

411 if bound.opening_bounds: 

412 openings.extend(bound.opening_bounds) 

413 for shape in convex_shapes: 

414 # loop through all new created convex shapes (which are subshapes 

415 # of the original bound) and copy the original boundary to keep 

416 # their properties. This new_bound has its own unique guid. 

417 # bound_shape and bound_area are modified to the new_convex shape. 

418 new_bound = self.create_new_boundary(bound, shape) 

419 if openings: 

420 new_bound.opening_bounds = [] 

421 for opening in openings: 

422 # map the openings to the new parent surface 

423 distance = BRepExtrema_DistShapeShape( 

424 new_bound.bound_shape, opening.bound_shape, 

425 Extrema_ExtFlag_MIN).Value() 

426 if distance < 1e-3: 

427 new_bound.opening_bounds.append(opening) 

428 opening.parent_bound = new_bound 

429 # check and fix surface normal if needed 

430 if not all([abs(i) < 1e-3 for i in ( 

431 (new_bound.bound_normal - bound.bound_normal).Coord())]): 

432 new_bound.bound_shape = PyOCCTools.flip_orientation_of_face( 

433 new_bound.bound_shape) 

434 new_bound.bound_normal = PyOCCTools.simple_face_normal( 

435 new_bound.bound_shape) 

436 # handle corresponding boundary (related_bound) 

437 if (related_bound and bound.related_bound.ifc.RelatingSpace.is_a( 

438 'IfcSpace')) and not bound.ifc.Description == '2b': 

439 distance = BRepExtrema_DistShapeShape( 

440 bound.bound_shape, related_bound.bound_shape, 

441 Extrema_ExtFlag_MIN).Value() 

442 related_bound.non_convex_guid = related_bound.guid 

443 # move shape of the current bound to the position of the 

444 # related bound if they have not been at the same position 

445 # before. 

446 if distance > 1e-3: 

447 new_rel_shape = \ 

448 PyOCCTools.move_bound_in_direction_of_normal( 

449 new_bound, distance, reverse=False) 

450 else: 

451 new_rel_shape = new_bound.bound_shape 

452 # assign bound_shape to related_bound, flip surface 

453 # orientation and recompute bound_normal and bound_area. 

454 new_rel_bound = self.create_new_boundary( 

455 related_bound, new_rel_shape) 

456 new_rel_bound.bound_shape = PyOCCTools.flip_orientation_of_face( 

457 new_rel_bound.bound_shape) 

458 new_rel_bound.bound_normal = PyOCCTools.simple_face_normal( 

459 new_rel_bound.bound_shape) 

460 # new_rel_bound.reset('bound_area') 

461 # new_rel_bound.bound_area = new_rel_bound.bound_area 

462 # handle opening bounds of related bound 

463 if new_bound.opening_bounds: 

464 for op in new_bound.opening_bounds: 

465 if not op.related_bound: 

466 continue 

467 new_rel_bound.opening_bounds.append(op.related_bound) 

468 op.related_bound.parent_bound = new_rel_bound 

469 new_bound.related_bound = new_rel_bound 

470 new_rel_bound.related_bound = new_bound 

471 new_space_boundaries.append(new_rel_bound) 

472 new_space_boundaries.append(new_bound) 

473 return new_space_boundaries 

474 

475 def split_non_convex_shadings(self, elements: dict, 

476 spatial_bounds: list[SpaceBoundary]): 

477 """Split non_convex shadings to convex shapes. 

478 

479 Args: 

480 elements: dict[guid: element] 

481 spatial_bounds: list of SpaceBoundary, that are connected to an 

482 ExternalSpatialElement 

483 """ 

484 # only considers the first spatial element for now. Extend this if 

485 # needed. 

486 spatial_elem = filter_elements(elements, ExternalSpatialElement)[0] 

487 for spatial in spatial_bounds: 

488 if is_convex_no_holes(spatial.bound_shape): 

489 continue 

490 try: 

491 convex_shapes = convex_decomposition(spatial.bound_shape) 

492 except Exception as ex: 

493 logger.warning(f"Unexpected {ex}. Converting shading bound " 

494 f"{spatial.guid} to convex shape failed. " 

495 f"{type(ex)}") 

496 new_space_boundaries = self.create_new_convex_bounds(convex_shapes, 

497 spatial) 

498 spatial_bounds.remove(spatial) 

499 if spatial in spatial_elem.space_boundaries: 

500 spatial_elem.space_boundaries.remove(spatial) 

501 for new_bound in new_space_boundaries: 

502 spatial_bounds.append(new_bound) 

503 spatial_elem.space_boundaries.append(new_bound)