Coverage for bim2sim/tasks/bps/enrich_use_cond.py: 93%

135 statements  

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

1from typing import Union, Dict 

2 

3from bim2sim.kernel.decision import ListDecision, DecisionBunch 

4from bim2sim.elements.bps_elements import ThermalZone 

5from bim2sim.tasks.base import ITask 

6from bim2sim.utilities.common_functions import get_use_conditions_dict, \ 

7 get_pattern_usage, wildcard_match, filter_elements 

8from bim2sim.tasks.base import Playground 

9from bim2sim.sim_settings import BuildingSimSettings 

10from bim2sim.utilities.types import AttributeDataSource 

11 

12 

13class EnrichUseConditions(ITask): 

14 """Enriches Use Conditions of thermal zones 

15 based on decisions and translation of zone names""" 

16 

17 reads = ('elements',) 

18 

19 def __init__(self, playground: Playground): 

20 super().__init__(playground) 

21 self.enriched_tz: list = [] 

22 self.use_conditions: dict = {} 

23 

24 def run(self, elements: dict): 

25 """Enriches Use Conditions of thermal zones and central AHU settings. 

26 

27 Enrichment data in the files commonUsages.json and UseConditions.json 

28 is taken from TEASER. The underlying data comes from DIN 18599-10 and 

29 SIA 2024. 

30 """ 

31 tz_elements = filter_elements(elements, 'ThermalZone', True) 

32 # case no thermal zones found 

33 if len(tz_elements) == 0: 

34 self.logger.warning("Found no spaces to enrich") 

35 else: 

36 custom_use_cond_path = ( 

37 self.playground.sim_settings.prj_use_conditions) 

38 custom_usage_path = \ 

39 self.playground.sim_settings.prj_custom_usages 

40 

41 self.logger.info("enriches thermal zones usage") 

42 self.use_conditions = get_use_conditions_dict(custom_use_cond_path) 

43 pattern_usage = get_pattern_usage(self.use_conditions, 

44 custom_usage_path) 

45 final_usages = yield from self.enrich_usages( 

46 pattern_usage, tz_elements) 

47 for tz, usage in final_usages.items(): 

48 orig_usage = tz.usage 

49 tz.usage = usage 

50 self.load_usage(tz) 

51 # overwrite loaded heating and cooling profiles with 

52 # template values if setpoints_from_template == True 

53 if self.playground.sim_settings.setpoints_from_template: 

54 tz.heating_profile = \ 

55 self.use_conditions[usage]['heating_profile'] 

56 tz.cooling_profile = \ 

57 self.use_conditions[usage]['cooling_profile'] 

58 # set maintained illuminance from sim_setting 

59 tz.use_maintained_illuminance = ( 

60 self.playground.sim_settings.use_maintained_illuminance, 

61 AttributeDataSource.enrichment) 

62 # reset lighting_power if it was calculated before 

63 tz.reset('lighting_power') 

64 self.enriched_tz.append(tz) 

65 self.logger.info('Enrich ThermalZone from IfcSpace with ' 

66 'original usage "%s" with usage "%s"', 

67 orig_usage, usage) 

68 # set heating and cooling based on sim settings configuration 

69 building_elements = filter_elements(elements, 'Building') 

70 self.overwrite_heating_cooling_ahu_by_settings( 

71 tz_elements, building_elements, self.playground.sim_settings) 

72 

73 

74 

75 @staticmethod 

76 def overwrite_heating_cooling_ahu_by_settings( 

77 tz_elements: dict, 

78 bldg_elements: list, 

79 sim_settings: BuildingSimSettings) -> None: 

80 """Set HVAC settings for thermal zones based on simulation settings. 

81 

82 Updates heating, cooling, and AHU usage for all thermal zones 

83 according to the provided simulation settings. 

84 

85 Args: 

86 tz_elements: Dictionary of thermal zone elements 

87 bldg_elements: List of building elements 

88 sim_settings: Building simulation settings 

89 """ 

90 # Apply settings to all thermal zones 

91 for tz in tz_elements.values(): 

92 if sim_settings.heating_tz_overwrite is not None: 

93 tz.with_heating = (sim_settings.heating_tz_overwrite, 

94 AttributeDataSource.enrichment) 

95 if sim_settings.cooling_tz_overwrite is not None: 

96 tz.with_cooling = (sim_settings.cooling_tz_overwrite, 

97 AttributeDataSource.enrichment) 

98 if sim_settings.ahu_tz_overwrite is not None: 

99 tz.with_ahu = (sim_settings.ahu_tz_overwrite, 

100 AttributeDataSource.enrichment) 

101 if sim_settings.base_infiltration_rate_overwrite is not None: 

102 tz.base_infiltration = ( 

103 sim_settings.base_infiltration_rate_overwrite, 

104 AttributeDataSource.enrichment) 

105 if sim_settings.use_constant_infiltration_overwrite is not None: 

106 tz.use_constant_infiltration = ( 

107 sim_settings.use_constant_infiltration_overwrite, 

108 AttributeDataSource.enrichment) 

109 

110 # overwrite building AHU settings if sim_settings are used 

111 for building in bldg_elements: 

112 if sim_settings.ahu_heating_overwrite is not None: 

113 building.ahu_heating = ( 

114 sim_settings.ahu_heating_overwrite, 

115 AttributeDataSource.enrichment) 

116 if sim_settings.ahu_cooling_overwrite is not None: 

117 building.ahu_cooling = ( 

118 sim_settings.ahu_cooling_overwrite, 

119 AttributeDataSource.enrichment) 

120 if sim_settings.ahu_humidification_overwrite is not None: 

121 building.ahu_humidification = ( 

122 sim_settings.ahu_humidification_overwrite, 

123 AttributeDataSource.enrichment) 

124 if sim_settings.ahu_dehumidification_overwrite is not None: 

125 building.ahu_dehumidification = ( 

126 sim_settings.ahu_dehumidification_overwrite, 

127 AttributeDataSource.enrichment) 

128 if sim_settings.ahu_heat_recovery_overwrite is not None: 

129 building.ahu_heat_recovery = ( 

130 sim_settings.ahu_heat_recovery_overwrite, 

131 AttributeDataSource.enrichment) 

132 if sim_settings.ahu_heat_recovery_efficiency_overwrite is not None: 

133 building.ahu_heat_recovery_efficiency = ( 

134 sim_settings.ahu_heat_recovery_efficiency_overwrite, 

135 AttributeDataSource.enrichment) 

136 

137 # reset with_ahu on building level to make sure that _check_tz_ahu 

138 # is performed again 

139 building.reset('with_ahu') 

140 

141 

142 

143 @staticmethod 

144 def list_decision_usage(tz: ThermalZone, choices: list) -> ListDecision: 

145 """decision to select an usage that matches the zone name 

146 

147 Args: 

148 tz: bim2sim ThermalZone element 

149 choices: list of possible answers 

150 Returns: 

151 usage_decision: ListDecision to find the correct usage 

152 """ 

153 usage_decision = ListDecision("Which usage does the Space %s have?" % 

154 (str(tz.usage)), 

155 choices=choices, 

156 key='usage_'+str(tz.usage), 

157 related=tz, 

158 global_key="%s_%s.BpsUsage" % 

159 (type(tz).__name__, tz.guid), 

160 allow_skip=False, 

161 live_search=True) 

162 return usage_decision 

163 

164 @staticmethod 

165 def office_usage(tz: ThermalZone) -> Union[str, list]: 

166 """function to determine which office usage is best fitting" 

167 

168 The used enrichment for usage conditions come from DIN 18599-10. This 

169 standard offers 3 types of office usages: 

170 * Single office (1 workplace) 

171 * Group office (2 - 6 workplaces) 

172 * Open open offices (> 6 workplaces) 

173 

174 Based on the standards given medium occupancy density the following area 

175 sections are defined. 

176 * Single office < 14 m2 

177 * Group office [14m2; 70 m2] 

178 (70 m² is lower bound from open plan office) 

179 * Open plan office > 70 m² 

180 

181 Args: 

182 tz: bim2sim thermalzone element 

183 Returns 

184 matching usage as string or a list of str of no fitting 

185 usage could be found 

186 """ 

187 

188 default_matches = ["Single office", 

189 "Group Office (between 2 and 6 employees)", 

190 "Open-plan Office (7 or more employees)"] 

191 if tz.gross_area: 

192 area = tz.gross_area.m 

193 if area < 14: 

194 return default_matches[0] 

195 elif 14 <= area <= 70: 

196 return default_matches[1] 

197 else: 

198 return default_matches[2] 

199 # case area not available 

200 else: 

201 return default_matches 

202 

203 @classmethod 

204 def enrich_usages( 

205 cls, 

206 pattern_usage: dict, 

207 thermal_zones: Dict[str, ThermalZone]) -> Dict[str, ThermalZone]: 

208 """Sets the usage of the given thermal_zones and enriches them. 

209 

210 Looks for fitting usages in assets/enrichment/usage based on the given 

211 usage of a zone in the IFC. The way the usage is obtained is described 

212 in the ThermalZone classes attribute "usage". 

213 The following data is taken into account: 

214 commonUsages.json: typical translations for the existing usage data 

215 customUsages<prj_name>.json: project specific translations that can 

216 be stored for easier simulation. 

217 

218 Args: 

219 pattern_usage: Dict with custom and common pattern 

220 thermal_zones: dict with tz elements guid as key and the element 

221 itself as value 

222 Returns: 

223 final_usages: key: str of usage type, value: ThermalZone element 

224 

225 """ 

226 # selected_usage = {} 

227 final_usages = {} 

228 for tz in list(thermal_zones.values()): 

229 orig_usage = str(tz.usage) 

230 if orig_usage in pattern_usage: 

231 final_usages[tz] = orig_usage 

232 else: 

233 matches = [] 

234 list_org = tz.usage.replace(' (', ' ').replace(')', ' '). \ 

235 replace(' -', ' ').replace(', ', ' ').split() 

236 for usage in pattern_usage.keys(): 

237 # check custom first 

238 if "custom" in pattern_usage[usage]: 

239 for cus_usage in pattern_usage[usage]["custom"]: 

240 # if cus_usage == tz.usage: 

241 if wildcard_match(cus_usage, tz.usage): 

242 if usage not in matches: 

243 matches.append(usage) 

244 # if not found in custom, continue with common 

245 if len(matches) == 0: 

246 for i in pattern_usage[usage]["common"]: 

247 for i_name in list_org: 

248 if i.match(i_name): 

249 if usage not in matches: 

250 matches.append(usage) 

251 # if just one match 

252 if len(matches) == 1: 

253 # case its an office 

254 if 'office_function' == matches[0]: 

255 office_use = cls.office_usage(tz) 

256 if isinstance(office_use, list): 

257 final_usages[tz] = cls.list_decision_usage( 

258 tz, office_use) 

259 else: 

260 final_usages[tz] = office_use 

261 # other zone usage 

262 else: 

263 final_usages[tz] = matches[0] 

264 # if no matches given forward all (for decision) 

265 elif len(matches) == 0: 

266 matches = list(pattern_usage.keys()) 

267 if len(matches) > 1: 

268 final_usages[tz] = cls.list_decision_usage( 

269 tz, matches) 

270 # selected_usage[orig_usage] = tz.usage 

271 # collect decisions 

272 usage_dec_bunch = DecisionBunch() 

273 for tz, use_or_dec in final_usages.items(): 

274 if isinstance(use_or_dec, ListDecision): 

275 usage_dec_bunch.append(use_or_dec) 

276 # remove duplicate decisions 

277 unique_decisions, doubled_decisions = usage_dec_bunch.get_reduced_bunch( 

278 criteria='key') 

279 yield unique_decisions 

280 answers = unique_decisions.to_answer_dict() 

281 # combine answers and not answered decision 

282 for dec in doubled_decisions: 

283 final_usages[dec.related] = answers[dec.key] 

284 for dec in unique_decisions: 

285 final_usages[dec.related] = dec.value 

286 # set usages 

287 return final_usages 

288 

289 # def one_zone_usage(self, thermal_zones: dict): 

290 # """defines an usage to all the building - since its a singular zone""" 

291 # usage_decision = ListDecision("Which usage does the one_zone_building" 

292 # " %s have?", 

293 # choices=list(UseConditions.keys()), 

294 # global_key="one_zone_usage", 

295 # allow_skip=False, 

296 # allow_load=True, 

297 # allow_save=True, 

298 # quick_decide=not True) 

299 # usage_decision.decide() 

300 # for tz in list(thermal_zones.values()): 

301 # tz.usage = usage_decision.value 

302 # self.enriched_tz.append(tz) 

303 

304 def load_usage(self, tz: ThermalZone): 

305 """loads the usage of the corresponding ThermalZone. 

306 

307 Loads the usage from the statistical data in assets/enrichment/usage. 

308 

309 Args: 

310 tz: bim2sim ThermalZone element 

311 """ 

312 use_condition = self.use_conditions[tz.usage] 

313 for attr, value in use_condition.items(): 

314 # avoid to overwrite attrs present on the element 

315 if getattr(tz, attr) is None: 

316 value = self.value_processing(value) 

317 setattr(tz, attr, value) 

318 

319 @staticmethod 

320 def value_processing(value: float): 

321 """""" 

322 if isinstance(value, dict): 

323 values = next(iter(value.values())) 

324 return values[0]/values[1] 

325 else: 

326 return value