Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/create_result_df.py: 0%

133 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 10:24 +0000

1import json 

2from pathlib import Path 

3 

4import pandas as pd 

5import pint_pandas 

6from geomeppy import IDF 

7from pint_pandas import PintArray 

8 

9from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.task import \ 

10 IdfPostprocessing 

11from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.utils import \ 

12 PostprocessingUtils 

13from bim2sim.tasks.base import ITask 

14from bim2sim.elements.mapping.units import ureg 

15from bim2sim.utilities.common_functions import filter_elements 

16 

17bim2sim_energyplus_mapping_base = { 

18 "NOT_AVAILABLE": "heat_demand_total", 

19 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total " 

20 "Heating Rate [W](Hourly)": "heat_demand_rooms", 

21 "NOT_AVAILABLE": "cool_demand_total", 

22 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total " 

23 "Cooling Rate [W](Hourly)": "cool_demand_rooms", 

24 "DistrictHeating:HVAC [J](Hourly)": "heat_energy_total", 

25 "DistrictCooling:HVAC [J](Hourly) ": "cool_energy_total", 

26 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total " 

27 "Heating Energy [J](Hourly)": 

28 "heat_energy_rooms", 

29 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total " 

30 "Cooling Energy [J](Hourly)": 

31 "cool_energy_rooms", 

32 "Environment:Site Outdoor Air Drybulb Temperature [C](Hourly)": 

33 "air_temp_out", 

34 "SPACEGUID:Zone Operative Temperature [C](Hourly)": 

35 "operative_temp_rooms", 

36 "SPACEGUID:Zone Mean Air Temperature [C](Hourly)": "air_temp_rooms", 

37 "SPACEGUID:Zone Electric Equipment Total Heating Rate [W](Hourly)": "internal_gains_machines_rooms", 

38 "SPACEGUID:Zone People Total Heating Rate [W](Hourly)": "internal_gains_persons_rooms", 

39 "SPACEGUID:Zone People Occupant Count [](Hourly)": "n_persons_rooms", 

40 "SPACEGUID:Zone Lights Total Heating Rate [W](Hourly)": "internal_gains_lights_rooms", 

41 "SPACEGUID:Zone Infiltration Air Change Rate [ach](Hourly)": "infiltration_rooms", 

42 "SPACEGUID:Zone Ventilation Standard Density Volume Flow Rate [m3/s](Hourly)": "mech_ventilation_rooms", 

43 "SPACEGUID:Zone Thermostat Heating Setpoint Temperature [C](Hourly)": "heat_set_rooms", 

44 "SPACEGUID:Zone Thermostat Cooling Setpoint Temperature [C](Hourly)": "cool_set_rooms", 

45 "BOUNDGUID:Surface Inside Face Temperature [C](Hourly)": "surf_inside_temp", 

46} 

47 

48pint_pandas.PintType.ureg = ureg 

49unit_mapping = { 

50 "heat_demand": ureg.watt, 

51 "cool_demand": ureg.watt, 

52 "heat_energy": ureg.joule, 

53 "cool_energy": ureg.joule, 

54 "operative_temp": ureg.degree_Celsius, 

55 "surf_inside_temp": ureg.degree_Celsius, 

56 "air_temp": ureg.degree_Celsius, 

57 "heat_set": ureg.degree_Celsius, 

58 "cool_set": ureg.degree_Celsius, 

59 "internal_gains": ureg.watt, 

60 "n_persons": ureg.dimensionless, 

61 "infiltration": ureg.hour ** (-1), 

62 "mech_ventilation": (ureg.meter ** 3) / ureg.second, 

63} 

64final_units = { 

65 "heat_energy": ureg.watthour, 

66 "cool_energy": ureg.watthour, 

67} 

68 

69 

70class CreateResultDF(ITask): 

71 """This ITask creates a result dataframe for EnergyPlus BEPS simulations 

72 

73 See detailed explanation in the run function below. 

74 """ 

75 

76 reads = ('idf', 'sim_results_path', 'elements') 

77 touches = ('df_finals',) 

78 

79 def run(self, idf: IDF, sim_results_path: Path, elements: dict) \ 

80 -> dict[str: pd.DataFrame]: 

81 """ Create a result DataFrame for EnergyPlus BEPS results. 

82 

83 This function transforms the EnergyPlus simulation results to the 

84 general result data format used in this bim2sim project. The 

85 simulation results stored in the EnergyPlus result file ( 

86 "eplusout.csv") are shifted by one hour to match the simulation 

87 results of the modelica simulation. Afterwards, the simulation 

88 results are formatted to match the bim2sim dataframe format. 

89 

90 Args: 

91 idf (IDF): eppy idf 

92 sim_results_path (Path): path to the simulation results from 

93 EnergyPlus 

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

95 holds preprocessed elements including space boundaries. 

96 Returns: 

97 df_finals (dict): dictionary in the format 

98 dict[str(project name): pd.DataFrame], final dataframe 

99 that holds only relevant data, with generic `bim2sim` names and 

100 index in form of MM/DD-hh:mm:ss 

101 

102 """ 

103 # ToDO handle multiple buildings/ifcs #35 

104 df_finals = {} 

105 if not self.playground.sim_settings.create_plots: 

106 self.logger.warning("Skipping task CreateResultDF as sim_setting " 

107 "'create_plots' is set to False and no " 

108 "DataFrame ist needed.") 

109 return df_finals, 

110 raw_csv_path = sim_results_path / self.prj_name / 'eplusout.csv' 

111 # TODO @Veronika: the zone_dict.json can be removed and instead the 

112 # elements structure can be used to get the zone guids 

113 zone_dict_path = sim_results_path / self.prj_name / 'zone_dict.json' 

114 if not zone_dict_path.exists(): 

115 IdfPostprocessing.write_zone_names(idf, elements, 

116 sim_results_path / self.prj_name) 

117 with open(zone_dict_path) as j: 

118 zone_dict = json.load(j) 

119 

120 # create dict for mapping surfaces to spaces 

121 space_bound_dict = {} 

122 space_bound_renamed_dict = {} 

123 spaces = filter_elements(elements, 'ThermalZone') 

124 for space in spaces: 

125 space_guids = [] 

126 for bound in space.space_boundaries: 

127 if isinstance(bound, str): 

128 space_guids.append(bound) 

129 else: 

130 space_guids.append(bound.guid) 

131 # rename space boundaries according to their surface orientation 

132 # for better identification in surface temperature plots. 

133 space_bound_renamed_dict[space.guid] = self.oriented_surface_names( 

134 idf, 

135 space.guid) 

136 space_bound_dict[space.guid] = space_guids 

137 with open(sim_results_path / self.prj_name / 'space_bound_dict.json', 

138 'w+') as file1: 

139 json.dump(space_bound_dict, file1, indent=4) 

140 with open(sim_results_path / self.prj_name / 

141 'space_bound_renamed_dict.json', 

142 'w+') as file2: 

143 json.dump(space_bound_renamed_dict, file2, indent=4) 

144 

145 df_original = PostprocessingUtils.read_csv_and_format_datetime( 

146 raw_csv_path) 

147 df_original = ( 

148 PostprocessingUtils.shift_dataframe_to_midnight(df_original)) 

149 df_final = self.format_dataframe(df_original, zone_dict, 

150 space_bound_dict) 

151 df_finals[self.prj_name] = df_final 

152 

153 return df_finals, 

154 

155 def format_dataframe( 

156 self, df_original: pd.DataFrame, zone_dict: dict, 

157 space_bound_dict: dict) -> pd.DataFrame: 

158 """Formats the dataframe to generic bim2sim output structure. 

159 

160 This function: 

161 - adds the space GUIDs to the results 

162 - selects only the selected simulation outputs from the result 

163 

164 Args: 

165 df_original: original dataframe directly taken from simulation 

166 zone_dict: dictionary with all zones, in format {GUID : Zone Usage} 

167 space_bound_dict: dictionary with space_guid and a list of space 

168 boundary guids of this space. 

169 Returns: 

170 df_final: converted dataframe in `bim2sim` result structure 

171 """ 

172 bim2sim_energyplus_mapping = self.map_zonal_results( 

173 bim2sim_energyplus_mapping_base, zone_dict, space_bound_dict, 

174 self.playground.sim_settings.plot_singe_zone_guid) 

175 # select only relevant columns 

176 short_list = \ 

177 list(bim2sim_energyplus_mapping.keys()) 

178 short_list.remove('NOT_AVAILABLE') 

179 df_final = df_original[df_original.columns[ 

180 df_original.columns.isin(short_list)]].rename( 

181 columns=bim2sim_energyplus_mapping) 

182 

183 # convert negative cooling demands and energies to absolute values 

184 energy_and_demands = df_final.filter(like='energy').columns.union( 

185 df_final.filter(like='demand').columns) 

186 df_final[energy_and_demands].abs() 

187 heat_demand_columns = df_final.filter(like='heat_demand') 

188 cool_demand_columns = df_final.filter(like='cool_demand') 

189 df_final['heat_demand_total'] = heat_demand_columns.sum(axis=1) 

190 df_final['cool_demand_total'] = cool_demand_columns.sum(axis=1) 

191 # handle units 

192 for column in df_final: 

193 for key, unit in unit_mapping.items(): 

194 if key in column: 

195 df_final[column] = PintArray(df_final[column], unit) 

196 for key, unit in final_units.items(): 

197 if key in column: 

198 df_final[column] = df_final[column].pint.to(unit) 

199 

200 return df_final 

201 

202 def select_wanted_results(self): 

203 """Selected only the wanted outputs based on sim_setting sim_results""" 

204 bim2sim_energyplus_mapping = bim2sim_energyplus_mapping_base.copy() 

205 for key, value in bim2sim_energyplus_mapping_base.items(): 

206 if value not in self.playground.sim_settings.sim_results: 

207 del bim2sim_energyplus_mapping[key] 

208 return bim2sim_energyplus_mapping 

209 

210 @staticmethod 

211 def map_zonal_results(bim2sim_energyplus_mapping_base, zone_dict, 

212 space_bound_dict=None, plot_single_zone_guid=None): 

213 """Add zone/space guids/names to mapping dict. 

214 

215 EnergyPlus outputs the results referencing to the IFC-GlobalId. This 

216 function adds the real zone/space guids or 

217 aggregation names to the dict for easy readable results. 

218 Rooms are mapped with their space GUID, aggregated zones are mapped 

219 with their zone name. The mapping between zones and rooms can be taken 

220 from tz_mapping.json file with can be found in export directory. 

221 

222 Args: 

223 bim2sim_energyplus_mapping_base: Holds the mapping between 

224 simulation outputs and generic `bim2sim` output names. 

225 zone_dict: dictionary with all zones, in format {GUID : Zone Usage} 

226 space_bound_dict: dictionary mapping space guids and their bounds 

227 plot_single_zone_guid: guid of single space that should be analyzed 

228 Returns: 

229 dict: A mapping between simulation results and space guids, with 

230 appropriate adjustments for aggregated zones. 

231 

232 """ 

233 bim2sim_energyplus_mapping = {} 

234 space_guid_list = list(zone_dict.keys()) 

235 for key, value in bim2sim_energyplus_mapping_base.items(): 

236 # add entry for each room/zone 

237 if "SPACEGUID" in key: 

238 # TODO write case sensitive GUIDs into dataframe 

239 for i, space_guid in enumerate(space_guid_list): 

240 new_key = key.replace("SPACEGUID", space_guid.upper()) 

241 # todo: according to #497, names should keep a _zone_ flag 

242 new_value = value.replace("rooms", 'rooms_' + space_guid) 

243 bim2sim_energyplus_mapping[new_key] = new_value 

244 elif "BOUNDGUID" in key and space_bound_dict is not None: 

245 for i, space in enumerate(space_bound_dict): 

246 if plot_single_zone_guid and \ 

247 space not in plot_single_zone_guid: 

248 # avoid loading space boundary data of multiple 

249 # zones unless all zones should be considered to 

250 # avoid an unreasonably large dataframe 

251 continue 

252 for bound in space_bound_dict[space]: 

253 guid = bound 

254 new_key = key.replace("BOUNDGUID", guid.upper()) 

255 new_value = value.replace("temp", "temp_" + guid) 

256 bim2sim_energyplus_mapping[new_key] = new_value 

257 else: 

258 bim2sim_energyplus_mapping[key] = value 

259 return bim2sim_energyplus_mapping 

260 

261 @staticmethod 

262 def oriented_surface_names(idf, space_guid): 

263 

264 """ 

265 Identify surface names for each individual surface in a zone based on 

266 boundary conditions, constructions, and surface orientations for a 

267 proper identification in plots of surface variables (e.g., temperature) 

268 Args: 

269 idf: Eppy IDF 

270 space_guid: single space GUID 

271 

272 Returns: dictionary of renamed space boundaries 

273 

274 """ 

275 space_bounds_renamed = {} 

276 temp_name_list = [] 

277 for ib in idf.getobject("ZONE", space_guid).zonesurfaces: 

278 temp_name = None 

279 if ib is not None: 

280 try: 

281 az = PostprocessingUtils.true_azimuth(ib) 

282 except: 

283 az = None 

284 if az is None: 

285 continue 

286 for key in PostprocessingUtils.azimuth_orientations: 

287 if (float(key) - 22.5) <= az < (float(key) + 22.5): 

288 if ib.Outside_Boundary_Condition == 'Surface': 

289 temp_name = 'Inner' 

290 elif ib.Outside_Boundary_Condition == 'Outdoors': 

291 temp_name = 'Outer' 

292 elif ib.Outside_Boundary_Condition == 'Ground': 

293 temp_name = 'Ground' 

294 elif ib.Outside_Boundary_Condition == 'Adiabatic': 

295 temp_name = 'Adiabatic' 

296 else: 

297 temp_name = '' 

298 surface_type = ib.Surface_Type 

299 if ib.Construction_Name == '_AirWall': 

300 surface_type = ib.Construction_Name 

301 temp_name = temp_name + surface_type 

302 if ib.Surface_Type == 'Wall': 

303 temp_name = temp_name + "_" + PostprocessingUtils.azimuth_orientations[key] 

304 if temp_name not in temp_name_list: 

305 temp_name_list.append(temp_name) 

306 else: 

307 temp_count = len([True for s in temp_name_list if 

308 temp_name in s]) 

309 temp_name = temp_name + "_" + str(temp_count) 

310 space_bounds_renamed[ib.Name] = temp_name 

311 break 

312 else: 

313 continue 

314 return space_bounds_renamed