Coverage for bim2sim/elements/aggregation/bps_aggregations.py: 42%

219 statements  

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

1import logging 

2from typing import Set, TYPE_CHECKING 

3from ifcopenshell import guid 

4 

5from bim2sim.elements import bps_elements as bps 

6from bim2sim.elements.aggregation import AggregationMixin 

7from bim2sim.elements.bps_elements import InnerFloor, Roof, OuterWall, \ 

8 GroundFloor, InnerWall, Window, InnerDoor, OuterDoor, Slab, Wall, Door, \ 

9 ExtSpatialSpaceBoundary 

10from bim2sim.elements.mapping import attribute 

11from bim2sim.elements.mapping.units import ureg 

12from bim2sim.utilities.common_functions import filter_elements 

13from bim2sim.utilities.types import AttributeDataSource 

14 

15if TYPE_CHECKING: 

16 from bim2sim.elements.bps_elements import (BPSProduct, SpaceBoundary, 

17 ThermalZone) 

18logger = logging.getLogger(__name__) 

19 

20 

21class AggregatedThermalZone(AggregationMixin, bps.ThermalZone): 

22 """Aggregates thermal zones""" 

23 aggregatable_elements = {bps.ThermalZone} 

24 

25 def __init__(self, elements, *args, **kwargs): 

26 super().__init__(elements, *args, **kwargs) 

27 # self.get_disaggregation_properties() 

28 self.bound_elements = self.bind_elements() 

29 self.bind_tz_to_storeys() 

30 self.bind_tz_to_building() 

31 self.description = '' 

32 # todo lump usage conditions of existing zones 

33 

34 def bind_elements(self): 

35 """elements binder for the resultant thermal zone""" 

36 bound_elements = [] 

37 for tz in self.elements: 

38 for inst in tz.bound_elements: 

39 if inst not in bound_elements: 

40 bound_elements.append(inst) 

41 return bound_elements 

42 

43 def bind_tz_to_storeys(self): 

44 storeys = [] 

45 for tz in self.elements: 

46 for storey in tz.storeys: 

47 if storey not in storeys: 

48 storeys.append(storey) 

49 if self not in storey.thermal_zones: 

50 storey.thermal_zones.append(self) 

51 if tz in storey.thermal_zones: 

52 storey.thermal_zones.remove(tz) 

53 self.storeys = storeys 

54 

55 def bind_tz_to_building(self): 

56 # there should be only one building, but to be sure use list and check 

57 buildings = [] 

58 for tz in self.elements: 

59 building = tz.building 

60 if building not in buildings: 

61 buildings.append(building) 

62 if self not in building.thermal_zones: 

63 building.thermal_zones.append(self) 

64 if tz in building.thermal_zones: 

65 building.thermal_zones.remove(tz) 

66 

67 if len(buildings) > 1: 

68 raise ValueError( 

69 f"An AggregatedThermalZone should only contain ThermalZone " 

70 f"elements from the same Building. But {self} contains " 

71 f"ThermalZone elements from {buildings}.") 

72 else: 

73 tz.building = buildings[0] 

74 

75 @classmethod 

76 def find_matches(cls, groups, elements): 

77 """creates a new thermal zone aggregation instance 

78 based on a previous filtering""" 

79 new_aggregations = [] 

80 thermal_zones = filter_elements(elements, 'ThermalZone') 

81 total_area = sum(i.gross_area for i in thermal_zones) 

82 for group, group_elements in groups.items(): 

83 aggregated_tz = None 

84 if group == 'one_zone_building': 

85 name = "Aggregated_%s" % group 

86 aggregated_tz = cls.create_aggregated_tz( 

87 name, group, group_elements, elements) 

88 elif group == 'not_combined': 

89 # last criterion no similarities 

90 area = sum(i.gross_area for i in groups[group]) 

91 if area / total_area <= 0.05: 

92 # Todo: usage and conditions criterion 

93 name = "Aggregated_not_neighbors" 

94 aggregated_tz = cls.create_aggregated_tz( 

95 name, group, group_elements, elements) 

96 else: 

97 # first criterion based on similarities 

98 # todo reuse this if needed but currently it doesn't seem so 

99 # group_name = re.sub('[\'\[\]]', '', group) 

100 group_name = group 

101 name = "Aggregated_%s" % group_name.replace(', ', '_') 

102 aggregated_tz = cls.create_aggregated_tz( 

103 name, group, group_elements, elements) 

104 if aggregated_tz: 

105 new_aggregations.append(aggregated_tz) 

106 return new_aggregations 

107 

108 @classmethod 

109 def create_aggregated_tz(cls, name, group, group_elements, elements): 

110 aggregated_tz = cls(group_elements) 

111 aggregated_tz.name = name 

112 aggregated_tz.description = group 

113 for tz in aggregated_tz.elements: 

114 if tz.guid in elements: 

115 del elements[tz.guid] 

116 elements[aggregated_tz.guid] = aggregated_tz 

117 return aggregated_tz 

118 

119 def _calc_net_volume(self, name) -> ureg.Quantity: 

120 """Calculate the thermal zone net volume""" 

121 return sum(tz.net_volume for tz in self.elements if 

122 tz.net_volume is not None) 

123 

124 net_volume = attribute.Attribute( 

125 functions=[_calc_net_volume], 

126 unit=ureg.meter ** 3, 

127 dependant_elements='elements' 

128 ) 

129 

130 def _intensive_calc(self, name) -> ureg.Quantity: 

131 """intensive properties getter - volumetric mean 

132 intensive_attributes = ['t_set_heat', 't_set_cool', 'height', 

133 'AreaPerOccupant', 'T_threshold_heating', 'activity_degree_persons', 

134 'fixed_heat_flow_rate_persons', 'internal_gains_moisture_no_people', 

135 'T_threshold_cooling', 'ratio_conv_rad_persons', 'machines', 

136 'ratio_conv_rad_machines', 'lighting_power', 

137 'ratio_conv_rad_machines', 'lighting_power', 'fixed_lighting_power', 

138 'ratio_conv_rad_lighting', 'maintained_illuminance', 

139 'lighting_efficiency_lumen', base_infiltration', 

140 'max_user_infiltration', 'min_ahu', 'max_ahu', 'persons']""" 

141 prop_sum = sum( 

142 getattr(tz, name) * tz.net_volume for tz in self.elements if 

143 getattr(tz, name) is not None and tz.net_volume is not None) 

144 return prop_sum / self.net_volume 

145 

146 def _intensive_list_calc(self, name) -> list: 

147 """intensive list properties getter - volumetric mean 

148 intensive_list_attributes = ['heating_profile', 'cooling_profile', 

149 'persons_profile', 'machines_profile', 'lighting_profile', 

150 'max_overheating_infiltration', 'max_summer_infiltration', 

151 'winter_reduction_infiltration']""" 

152 list_attrs = {'heating_profile': 24, 'cooling_profile': 24, 

153 'persons_profile': 24, 

154 'machines_profile': 24, 'lighting_profile': 24, 

155 'max_overheating_infiltration': 2, 

156 'max_summer_infiltration': 3, 

157 'winter_reduction_infiltration': 3} 

158 length = list_attrs[name] 

159 aux = [] 

160 for x in range(0, length): 

161 aux.append(sum( 

162 getattr(tz, name)[x] * tz.net_volume for tz in self.elements 

163 if getattr(tz, name) is not None and tz.net_volume is not None) 

164 / self.net_volume) 

165 return aux 

166 

167 def _extensive_calc(self, name) -> ureg.Quantity: 

168 """extensive properties getter 

169 intensive_attributes = ['gross_area', 'net_area', 'volume']""" 

170 return sum(getattr(tz, name) for tz in self.elements if 

171 getattr(tz, name) is not None) 

172 

173 def _bool_calc(self, name) -> bool: 

174 """bool properties getter 

175 bool_attributes = ['with_cooling', 'with_heating', 'with_ahu', 

176 'use_maintained_illuminance']""" 

177 # todo: log 

178 prop_bool = False 

179 for tz in self.elements: 

180 prop = getattr(tz, name) 

181 if prop is not None: 

182 if prop: 

183 prop_bool = True 

184 break 

185 return prop_bool 

186 

187 def _get_tz_usage(self, name) -> str: 

188 """usage properties getter""" 

189 return self.elements[0].usage 

190 

191 usage = attribute.Attribute( 

192 functions=[_get_tz_usage], 

193 ) 

194 # t_set_heat = attribute.Attribute( 

195 # functions=[_intensive_calc], 

196 # unit=ureg.degC 

197 # ) 

198 # todo refactor this to remove redundancy for units 

199 t_set_heat = bps.ThermalZone.t_set_heat.to_aggregation(_intensive_calc) 

200 

201 t_set_cool = attribute.Attribute( 

202 functions=[_intensive_calc], 

203 unit=ureg.degC, 

204 dependant_elements='elements' 

205 ) 

206 t_ground = attribute.Attribute( 

207 functions=[_intensive_calc], 

208 unit=ureg.degC, 

209 dependant_elements='elements' 

210 ) 

211 net_area = attribute.Attribute( 

212 functions=[_extensive_calc], 

213 unit=ureg.meter ** 2, 

214 dependant_elements='elements' 

215 ) 

216 gross_area = attribute.Attribute( 

217 functions=[_extensive_calc], 

218 unit=ureg.meter ** 2, 

219 dependant_elements='elements' 

220 ) 

221 gross_volume = attribute.Attribute( 

222 functions=[_extensive_calc], 

223 unit=ureg.meter ** 3, 

224 dependant_elements='elements' 

225 ) 

226 height = attribute.Attribute( 

227 functions=[_intensive_calc], 

228 unit=ureg.meter, 

229 dependant_elements='elements' 

230 ) 

231 area_per_occupant = attribute.Attribute( 

232 functions=[_intensive_calc], 

233 unit=ureg.meter ** 2, 

234 dependant_elements='elements' 

235 ) 

236 # use conditions 

237 with_cooling = attribute.Attribute( 

238 functions=[_bool_calc], 

239 dependant_elements='elements' 

240 ) 

241 with_heating = attribute.Attribute( 

242 functions=[_bool_calc], 

243 dependant_elements='elements' 

244 ) 

245 with_ahu = attribute.Attribute( 

246 functions=[_bool_calc], 

247 dependant_elements='elements' 

248 ) 

249 heating_profile = attribute.Attribute( 

250 functions=[_intensive_list_calc], 

251 dependant_elements='elements' 

252 ) 

253 cooling_profile = attribute.Attribute( 

254 functions=[_intensive_list_calc], 

255 dependant_elements='elements' 

256 ) 

257 persons = attribute.Attribute( 

258 functions=[_intensive_calc], 

259 dependant_elements='elements' 

260 ) 

261 T_threshold_heating = attribute.Attribute( 

262 functions=[_intensive_calc], 

263 dependant_elements='elements' 

264 ) 

265 activity_degree_persons = attribute.Attribute( 

266 functions=[_intensive_calc], 

267 dependant_elements='elements' 

268 ) 

269 fixed_heat_flow_rate_persons = attribute.Attribute( 

270 functions=[_intensive_calc], 

271 dependant_elements='elements' 

272 ) 

273 internal_gains_moisture_no_people = attribute.Attribute( 

274 functions=[_intensive_calc], 

275 dependant_elements='elements' 

276 ) 

277 T_threshold_cooling = attribute.Attribute( 

278 functions=[_intensive_calc], 

279 dependant_elements='elements' 

280 ) 

281 ratio_conv_rad_persons = attribute.Attribute( 

282 functions=[_intensive_calc], 

283 dependant_elements='elements' 

284 ) 

285 machines = attribute.Attribute( 

286 functions=[_intensive_calc], 

287 dependant_elements='elements' 

288 ) 

289 ratio_conv_rad_machines = attribute.Attribute( 

290 functions=[_intensive_calc], 

291 dependant_elements='elements' 

292 ) 

293 use_maintained_illuminance = attribute.Attribute( 

294 functions=[_bool_calc], 

295 dependant_elements='elements' 

296 ) 

297 lighting_power = attribute.Attribute( 

298 functions=[_intensive_calc], 

299 dependant_elements='elements' 

300 ) 

301 fixed_lighting_power = attribute.Attribute( 

302 functions=[_intensive_calc], 

303 dependant_elements='elements' 

304 ) 

305 ratio_conv_rad_lighting = attribute.Attribute( 

306 functions=[_intensive_calc], 

307 dependant_elements='elements' 

308 ) 

309 maintained_illuminance = attribute.Attribute( 

310 functions=[_intensive_calc], 

311 dependant_elements='elements' 

312 ) 

313 lighting_efficiency_lumen = attribute.Attribute( 

314 functions=[_intensive_calc], 

315 dependant_elements='elements' 

316 ) 

317 use_constant_infiltration = attribute.Attribute( 

318 functions=[_bool_calc], 

319 dependant_elements='elements' 

320 ) 

321 base_infiltration = attribute.Attribute( 

322 functions=[_intensive_calc], 

323 dependant_elements='elements' 

324 ) 

325 max_user_infiltration = attribute.Attribute( 

326 functions=[_intensive_calc], 

327 dependant_elements='elements' 

328 ) 

329 max_overheating_infiltration = attribute.Attribute( 

330 functions=[_intensive_list_calc], 

331 dependant_elements='elements' 

332 ) 

333 max_summer_infiltration = attribute.Attribute( 

334 functions=[_intensive_list_calc], 

335 dependant_elements='elements' 

336 ) 

337 winter_reduction_infiltration = attribute.Attribute( 

338 functions=[_intensive_list_calc], 

339 dependant_elements='elements' 

340 ) 

341 min_ahu = attribute.Attribute( 

342 functions=[_intensive_calc], 

343 dependant_elements='elements' 

344 ) 

345 max_ahu = attribute.Attribute( 

346 functions=[_intensive_calc], 

347 dependant_elements='elements' 

348 ) 

349 with_ideal_thresholds = attribute.Attribute( 

350 functions=[_bool_calc], 

351 dependant_elements='elements' 

352 ) 

353 persons_profile = attribute.Attribute( 

354 functions=[_intensive_list_calc], 

355 dependant_elements='elements' 

356 ) 

357 machines_profile = attribute.Attribute( 

358 functions=[_intensive_list_calc], 

359 dependant_elements='elements' 

360 ) 

361 lighting_profile = attribute.Attribute( 

362 functions=[_intensive_list_calc], 

363 dependant_elements='elements' 

364 ) 

365 

366 

367class SBDisaggregationMixin: 

368 guid_prefix = 'DisAgg_' 

369 disaggregatable_classes: Set['BPSProduct'] = set() 

370 thermal_zones = [] 

371 

372 def __init__(self, disagg_parent: 'BPSProduct', sbs: list['SpaceBoundary'] 

373 , *args, **kwargs): 

374 """ 

375 

376 Args: 

377 disagg_parent: Parent bim2sim element that was disaggregated 

378 """ 

379 super().__init__(*args, **kwargs) 

380 if self.disaggregatable_classes: 

381 received = {type(disagg_parent)} 

382 mismatch = received - self.disaggregatable_classes 

383 if mismatch: 

384 raise AssertionError("Can't aggregate %s from elements: %s" % 

385 (self.__class__.__name__, mismatch)) 

386 self.thermal_zones = [sb.bound_thermal_zone for sb in sbs] 

387 for tz in self.thermal_zones: 

388 if disagg_parent in tz.bound_elements: 

389 tz.bound_elements.remove(disagg_parent) 

390 tz.bound_elements.append(self) 

391 if sbs[0].related_bound and not isinstance( 

392 sbs[0].related_bound, ExtSpatialSpaceBoundary): 

393 # if the space boundary and its related_bound have different 

394 # bound_elements which are assigned to have the same 

395 # bound_element during disaggregation, the thermal zone must 

396 # get a reference to the newly assigned bound_element instead. 

397 if sbs[0].bound_element != sbs[0].related_bound.bound_element: 

398 if (sbs[0].related_bound.bound_element in 

399 sbs[0].related_bound.bound_thermal_zone.bound_elements): 

400 sbs[0].related_bound.bound_thermal_zone.bound_elements.remove( 

401 sbs[0].related_bound.bound_element) 

402 sbs[0].related_bound.bound_thermal_zone.bound_elements.append( 

403 self) 

404 for sb in sbs: 

405 # Only set disagg_parent if disagg_parent is the element of the SB 

406 # because otherwise we prevent creation of disaggregations for this 

407 # SB 

408 if disagg_parent == sb.bound_element: 

409 sb.disagg_parent = disagg_parent 

410 sb.bound_element = self 

411 # if sb.related_bound: 

412 # if not isinstance(sb.related_bound, ExtSpatialSpaceBoundary): 

413 # sb.related_bound.bound_element = self 

414 # sb.related_bound.disagg_parent = disagg_parent 

415 

416 # set references to other elements 

417 self.disagg_parent = disagg_parent 

418 self.disagg_parent.disaggregations.append(self) 

419 if len(sbs) > 2: 

420 logger.error(f'More than 2 SBs detected here (GUID: {self}.') 

421 if len(sbs) == 2: 

422 if abs(sbs[0].net_bound_area - sbs[1].net_bound_area).m > 0.001: 

423 logger.error(f'Large deviation in net bound area for SBs ' 

424 f'{sbs[0].guid} and {sbs[1].guid}') 

425 if abs(sbs[0].bound_area - sbs[1].bound_area).m > 0.001: 

426 logger.error(f'Large deviation in net bound area for SBs ' 

427 f'{sbs[0].guid} and {sbs[1].guid}') 

428 

429 # Get information from SB 

430 self.space_boundaries = sbs 

431 self.net_area = ( 

432 sbs[0].net_bound_area, AttributeDataSource.space_boundary) 

433 self.gross_area = ( 

434 sbs[0].bound_area, AttributeDataSource.space_boundary) 

435 self.opening_area = ( 

436 sbs[0].opening_area, AttributeDataSource.space_boundary) 

437 # get information from disagg_parent 

438 for att_name, value in disagg_parent.attributes.items(): 

439 if att_name not in ['net_area', 'gross_area', 'opening_area', 

440 'gross_volume', 'net_volume']: 

441 self.attributes[att_name] = value 

442 self.layerset = disagg_parent.layerset 

443 self.material = disagg_parent.material 

444 self.material_set = disagg_parent.material_set 

445 self.ifc = disagg_parent.ifc 

446 self.storeys = disagg_parent.storeys 

447 

448 @staticmethod 

449 def get_id(prefix=""): 

450 prefix_length = len(prefix) 

451 if prefix_length > 10: 

452 raise AttributeError("Max prefix length is 10!") 

453 ifcopenshell_guid = guid.new()[prefix_length+1:] 

454 return f"{prefix}{ifcopenshell_guid}" 

455 

456 

457class InnerFloorDisaggregated(SBDisaggregationMixin, InnerFloor): 

458 disaggregatable_classes = { 

459 InnerFloor, Slab, Roof, GroundFloor} 

460 

461 

462class GroundFloorDisaggregated(SBDisaggregationMixin, GroundFloor): 

463 disaggregatable_classes = { 

464 InnerFloor, Slab, Roof, GroundFloor} 

465 

466 

467class RoofDisaggregated(SBDisaggregationMixin, Roof): 

468 disaggregatable_classes = { 

469 InnerFloor, Slab, Roof, GroundFloor} 

470 

471 

472class InnerWallDisaggregated(SBDisaggregationMixin, InnerWall): 

473 disaggregatable_classes = { 

474 Wall, OuterWall, InnerWall} 

475 

476 

477class OuterWallDisaggregated(SBDisaggregationMixin, OuterWall): 

478 disaggregatable_classes = { 

479 Wall, OuterWall, InnerWall, InnerFloor} 

480 

481 

482class InnerDoorDisaggregated(SBDisaggregationMixin, InnerDoor): 

483 disaggregatable_classes = { 

484 Door, OuterDoor, InnerDoor} 

485 

486 

487class OuterDoorDisaggregated(SBDisaggregationMixin, OuterDoor): 

488 disaggregatable_classes = { 

489 Door, OuterDoor, InnerDoor} 

490 

491 

492class WindowDisaggregated(SBDisaggregationMixin, Window): 

493 disaggregatable_classes = {Window}