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

1import logging 

2import math 

3from typing import List, Union, Tuple, Dict 

4 

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 

9 

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) 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class CreateSpaceBoundaries(ITask): 

26 """Create space boundary elements from ifc. 

27 

28 See run function for further information on this module. """ 

29 

30 reads = ('ifc_files', 'elements') 

31 

32 def run(self, ifc_files: list, elements: dict): 

33 """Create space boundaries for elements from IfcRelSpaceBoundary. 

34 

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. 

44 

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 """ 

52 

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.") 

72 

73 self.add_bounds_to_elements(elements, space_boundaries) 

74 self.remove_elements_without_sbs(elements) 

75 

76 @staticmethod 

77 def remove_elements_without_sbs(elements: dict): 

78 """Remove elements that hold no Space Boundaries. 

79 

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] 

92 

93 @staticmethod 

94 def add_bounds_to_elements( 

95 elements: dict, space_boundaries: dict[str, SpaceBoundary]): 

96 """Add space boundaries to elements. 

97 

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. 

103 

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) 

131 

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. 

137 

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. 

141 

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 

199 

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. 

204 

205 This function returns all opening elements of the current related 

206 building element which is related to the current space boundary. 

207 

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 

219 

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 

227 

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. 

235 

236 This function returns the related opening boundary of another 

237 space boundary. 

238 

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 

269 

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. 

279 

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. 

293 

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 

370 

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. 

377 

378 This function instantiates space boundaries using given element class. 

379 Result is a list with the resulting valid elements. 

380 

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 

415 

416 return list(element_lst.values()) 

417 

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. 

422 

423 Connects resulting space boundary with the corresponding relating 

424 space (i.e., ThermalZone) and related building element (if given). 

425 

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 

433 

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) 

442 

443 @staticmethod 

444 def connect_element_to_zone(thermal_zone: ThermalZone, 

445 bound_element: IFCBased): 

446 """Connects related building element and corresponding thermal zone. 

447 

448 This function connects a thermal zone and its IFCBased related 

449 building elements. 

450 

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)