Coverage for bim2sim/plugins/PluginComfort/bim2sim_comfort/task/plot_comfort_results.py: 0%

271 statements  

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

1import json 

2import logging 

3from pathlib import Path 

4 

5import matplotlib as mpl 

6import numpy as np 

7import pandas as pd 

8from RWTHColors import ColorManager 

9from matplotlib import pyplot as plt 

10from matplotlib.colors import ListedColormap, Normalize 

11 

12from bim2sim.tasks.bps import PlotBEPSResults 

13 

14INCH = 2.54 

15 

16logger = logging.getLogger(__name__) 

17cm = ColorManager() 

18plt.rcParams.update(mpl.rcParamsDefault) 

19plt.style.use(['science', 'grid', 'rwth']) 

20plt.style.use(['science', 'no-latex']) 

21 

22# Update rcParams for font settings 

23plt.rcParams.update({ 

24 'font.size': 20, 

25 'font.family': 'sans-serif', # Use sans-serif font 

26 'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans', 'sans-serif'], # Specify sans-serif fonts 

27 'legend.frameon': True, 

28 'legend.facecolor': 'white', 

29 'legend.framealpha': 0.5, 

30 'legend.edgecolor': 'black', 

31 "lines.linewidth": 0.4, 

32 "text.usetex": False, # use inline math for ticks 

33 "pgf.rcfonts": True, 

34}) 

35 

36class PlotComfortResults(PlotBEPSResults): 

37 reads = ('df_finals', 'sim_results_path', 'ifc_files') 

38 final = True 

39 

40 def run(self, df_finals, sim_results_path, ifc_files): 

41 """Plots the results for BEPS simulations. 

42 

43 This holds pre configured functions to plot the results of the BEPS 

44 simulations with the EnergyPlus-based PluginComfort . 

45 

46 Args: 

47 df_finals: dict of final results where key is the building name and 

48 value is the dataframe holding the results for this building 

49 sim_results_path: base path where to store the plots 

50 ifc_files: bim2sim IfcFileClass holding the ifcopenshell ifc instance 

51 """ 

52 if not self.playground.sim_settings.create_plots: 

53 logger.info("Visualization of Comfort Results is skipped ...") 

54 return 

55 logger.info("Visualization of Comfort Results started ...") 

56 plot_single_guid = self.playground.sim_settings.plot_singe_zone_guid 

57 

58 

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

60 with open(zone_dict_path) as j: 

61 zone_dict = json.load(j) 

62 if plot_single_guid: 

63 logger.info("Check if plot_single_guid is valid space name.") 

64 if not plot_single_guid in zone_dict.keys(): 

65 plot_single_guid = '' 

66 logger.info("Requested plot_single_guid is not found in IFC " 

67 "file, plotting results for all spaces instead.") 

68 if self.playground.sim_settings.rename_plot_keys: 

69 with open(self.playground.sim_settings.rename_plot_keys_path) as rk: 

70 rename_keys = json.load(rk) 

71 zone_dict = self.rename_zone_usage(zone_dict, rename_keys) 

72 

73 

74 for bldg_name, df in df_finals.items(): 

75 export_path = sim_results_path / bldg_name / 'plots' 

76 if not export_path.exists(): 

77 export_path.mkdir(parents=False, exist_ok=False) 

78 # generate DIN EN 16798-1 adaptive comfort scatter plot and 

79 # return analysis of comfort categories for further plots 

80 if not plot_single_guid: 

81 cat_analysis = self.apply_en16798_to_all_zones(df, zone_dict, 

82 export_path) 

83 else: 

84 cat_analysis = self.apply_en16798_to_single_zone( 

85 df, zone_dict, export_path, plot_single_guid) 

86 # plot a barplot combined with table of comfort categories from 

87 # DIN EN 16798. 

88 self.table_bar_plot_16798(cat_analysis, export_path) 

89 

90 fanger_pmv = df[[col for col in df.columns if 'fanger_pmv' in col]] 

91 if plot_single_guid: 

92 fanger_pmv = fanger_pmv[[col for col in fanger_pmv.columns if 

93 plot_single_guid in col]] 

94 self.pmv_plot(fanger_pmv, export_path, 

95 f"pmv_{plot_single_guid}") 

96 for col in fanger_pmv.columns: 

97 # generate calendar plot for daily mean pmv results 

98 self.visualize_calendar(pd.DataFrame(fanger_pmv[col]), 

99 export_path, save_as='calendar_', 

100 add_title=True, 

101 color_only=True, figsize=[11, 12], 

102 zone_dict=zone_dict) 

103 

104 @staticmethod 

105 def rename_duplicates(dictionary): 

106 value_counts = {} 

107 renamed_dict = {} 

108 for key, value in dictionary.items(): 

109 if value in value_counts: 

110 value_counts[value] += 1 

111 new_value = f"{value}_{value_counts[value]}" 

112 else: 

113 value_counts[value] = 1 

114 new_value = value 

115 

116 renamed_dict[key] = new_value 

117 return renamed_dict 

118 

119 def rename_zone_usage(self, zone_dict, rename_keys): 

120 for key in zone_dict.keys(): 

121 for key2 in rename_keys.keys(): 

122 if zone_dict[key] == key2: 

123 zone_dict[key] = rename_keys[key2] 

124 zone_usage = self.rename_duplicates(zone_dict) 

125 return zone_usage 

126 

127 @staticmethod 

128 def pmv_plot(df, save_path, file_name): 

129 PlotBEPSResults.plot_dataframe(df, save_path=save_path, 

130 file_name=file_name, 

131 x_axis_title="Date", 

132 y_axis_title="PMV") 

133 

134 

135 def apply_en16798_to_all_zones(self, df, zone_dict, export_path): 

136 """Generate EN 16798 diagrams for all thermal zones. 

137 

138 """ 

139 logger.info("Plot DIN EN 16798 diagrams for all zones ...") 

140 

141 cat_analysis = pd.DataFrame() 

142 for guid, room_name in zone_dict.items(): 

143 temp_cat_analysis = None 

144 temp_cat_analysis = self.plot_new_en16798_adaptive_count( 

145 df, guid, room_name, export_path) 

146 cat_analysis = pd.concat([cat_analysis, temp_cat_analysis]) 

147 return cat_analysis 

148 

149 def apply_en16798_to_single_zone(self, df, zone_dict, export_path, 

150 zone_guid): 

151 logger.info(f"Plot DIN EN 16798 diagrams for zone {zone_guid} ...") 

152 

153 cat_analysis = pd.DataFrame() 

154 for guid, room_name in zone_dict.items(): 

155 if not guid == zone_guid: 

156 continue 

157 temp_cat_analysis = None 

158 temp_cat_analysis = self.plot_new_en16798_adaptive_count( 

159 df, guid, room_name, export_path) 

160 cat_analysis = pd.concat([cat_analysis, temp_cat_analysis]) 

161 return cat_analysis 

162 

163 @staticmethod 

164 def plot_new_en16798_adaptive_count(df, guid, room_name, export_path): 

165 """Plot EN 16798 diagram for thermal comfort categories for a single 

166 thermal zone. 

167 

168 """ 

169 logger.info(f"Plot DIN EN 16798 diagrams for zone {guid}: {room_name}.") 

170 

171 def is_within_thresholds_cat1_16798(row): 

172 if 10 <= row.iloc[0] <= 30: 

173 y_threshold1 = 0.33 * row.iloc[0] + 18.8 - 3 

174 y_threshold2 = 0.33 * row.iloc[0] + 18.8 + 2 

175 return y_threshold1 <= row.iloc[1] <= y_threshold2 

176 else: 

177 return False 

178 

179 def is_within_thresholds_cat2_16798(row): 

180 if 10 <= row.iloc[0] <= 30: 

181 y_threshold1a = 0.33 * row.iloc[0] + 18.8 - 4 

182 y_threshold1b = 0.33 * row.iloc[0] + 18.8 - 3 

183 y_threshold2a = 0.33 * row.iloc[0] + 18.8 + 2 

184 y_threshold2b = 0.33 * row.iloc[0] + 18.8 + 3 

185 return any([y_threshold1a <= row.iloc[1] <= y_threshold1b, 

186 y_threshold2a <= row.iloc[1] <= y_threshold2b]) 

187 else: 

188 return False 

189 

190 def is_within_thresholds_cat3_16798(row): 

191 if 10 <= row.iloc[0] <= 30: 

192 y_threshold1a = 0.33 * row.iloc[0] + 18.8 - 5 

193 y_threshold1b = 0.33 * row.iloc[0] + 18.8 - 4 

194 y_threshold2a = 0.33 * row.iloc[0] + 18.8 + 3 

195 y_threshold2b = 0.33 * row.iloc[0] + 18.8 + 4 

196 return any([y_threshold1a <= row.iloc[1] <= y_threshold1b, 

197 y_threshold2a <= row.iloc[1] <= y_threshold2b]) 

198 else: 

199 return False 

200 

201 def is_outside_thresholds_16798(row): 

202 if 10 <= row.iloc[0] <= 30: 

203 y_threshold1 = 0.33 * row.iloc[0] + 18.8 - 5 

204 y_threshold2 = 0.33 * row.iloc[0] + 18.8 + 4 

205 return any([y_threshold1 >= row.iloc[1], y_threshold2 

206 <= row.iloc[1]]) 

207 else: 

208 return False 

209 

210 lim_min = 10 

211 lim_max = 30 

212 

213 ot = df['operative_air_temp_rooms_' + guid] 

214 out_temp = df['site_outdoor_air_temp'] 

215 

216 merged_df = pd.merge(out_temp, ot, left_index=True, right_index=True) 

217 merged_df = merged_df.map(lambda x: x.m) 

218 filtered_df_cat1 = merged_df[ 

219 merged_df.apply(is_within_thresholds_cat1_16798, 

220 axis=1)] 

221 filtered_df_cat2 = merged_df[ 

222 merged_df.apply(is_within_thresholds_cat2_16798, 

223 axis=1)] 

224 filtered_df_cat3 = merged_df[ 

225 merged_df.apply(is_within_thresholds_cat3_16798, 

226 axis=1)] 

227 filtered_df_outside = merged_df[ 

228 merged_df.apply(is_outside_thresholds_16798, 

229 axis=1)] 

230 cat_analysis_dict = { 

231 'ROOM': room_name, 

232 'CAT1': len(filtered_df_cat1), 

233 'CAT2': len(filtered_df_cat2), 

234 'CAT3': len(filtered_df_cat3), 

235 'OUT': len(filtered_df_outside) 

236 } 

237 cat_analysis_df = pd.DataFrame(cat_analysis_dict, index=[0]) 

238 

239 analysis_file = export_path / 'DIN_EN_16798_analysis.csv' 

240 cat_analysis_df.to_csv(analysis_file, mode='a+', header=False, sep=';') 

241 

242 plt.figure(figsize=(13.2 / INCH, 8.3 / INCH)) 

243 

244 plt.scatter(filtered_df_cat1.iloc[:, 0], filtered_df_cat1.iloc[:, 1], 

245 s=0.1, 

246 color='green', marker=".") 

247 plt.scatter(filtered_df_cat2.iloc[:, 0], filtered_df_cat2.iloc[:, 1], 

248 s=0.1, 

249 color='orange', marker=".") 

250 plt.scatter(filtered_df_cat3.iloc[:, 0], filtered_df_cat3.iloc[:, 1], 

251 s=0.1, 

252 color='red', marker=".") 

253 plt.scatter(filtered_df_outside.iloc[:, 0], 

254 filtered_df_outside.iloc[:, 1], 

255 s=0.1, color='blue', label='OUT OF RANGE', marker=".") 

256 coord_cat1_low = [[10, 0.33 * 10 + 18.8 - 3.0], 

257 [30, 0.33 * 30 + 18.8 - 3.0]] 

258 coord_cat1_up = [[10, 0.33 * 10 + 18.8 + 2.0], 

259 [30, 0.33 * 30 + 18.8 + 2.0]] 

260 cc1lx, cc1ly = zip(*coord_cat1_low) 

261 cc1ux, cc1uy = zip(*coord_cat1_up) 

262 plt.plot(cc1lx, cc1ly, linestyle='dashed', color='green', 

263 label='DIN EN 16798-1: Thresholds Category I') 

264 plt.plot(cc1ux, cc1uy, linestyle='dashed', color='green') 

265 coord_cat2_low = [[10, 0.33 * 10 + 18.8 - 4.0], 

266 [30, 0.33 * 30 + 18.8 - 4.0]] 

267 coord_cat2_up = [[10, 0.33 * 10 + 18.8 + 3.0], 

268 [30, 0.33 * 30 + 18.8 + 3.0]] 

269 cc2lx, cc2ly = zip(*coord_cat2_low) 

270 cc2ux, cc2uy = zip(*coord_cat2_up) 

271 plt.plot(cc2lx, cc2ly, linestyle='dashed', color='orange', 

272 label='DIN EN 16798-1: Thresholds Category II') 

273 plt.plot(cc2ux, cc2uy, linestyle='dashed', color='orange') 

274 

275 coord_cat3_low = [[10, 0.33 * 10 + 18.8 - 5.0], 

276 [30, 0.33 * 30 + 18.8 - 5.0]] 

277 coord_cat3_up = [[10, 0.33 * 10 + 18.8 + 4.0], 

278 [30, 0.33 * 30 + 18.8 + 4.0]] 

279 cc3lx, cc3ly = zip(*coord_cat3_low) 

280 cc3ux, cc3uy = zip(*coord_cat3_up) 

281 plt.plot(cc3lx, cc3ly, linestyle='dashed', color='red', 

282 label='DIN EN 16798-1: Thresholds Category III') 

283 plt.plot(cc3ux, cc3uy, linestyle='dashed', color='red') 

284 

285 # Customize plot 

286 plt.xlabel('Running Mean Outdoor Temperature (\u00B0C)', 

287 fontsize=8) 

288 plt.ylabel('Operative Temperature (\u00B0C)', fontsize=8) 

289 plt.xlim([lim_min, lim_max]) 

290 plt.ylim([16.5, 35.5]) 

291 plt.grid() 

292 lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=8) 

293 plt.savefig( 

294 export_path / str('DIN_EN_16798_new_' + room_name + '.pdf')) 

295 

296 return cat_analysis_df 

297 

298 @staticmethod 

299 def table_bar_plot_16798(df, export_path): 

300 """Create bar plot with a table below for EN 16798 thermal comfort. 

301 

302 This function creates a bar plot with a table below along with the 

303 thermal comfort categories according to EN 16798. This table 

304 considers all hours of the day, not only the occupancy hours. 

305 

306 """ 

307 # with columns: 'ROOM', 'CAT1', 'CAT2', 'CAT3', 'OUT' 

308 logger.info(f"Plot DIN EN 16798 table bar plot all zones.") 

309 

310 rename_columns = { 

311 'CAT1': 'CAT I', 

312 'CAT2': 'CAT II', 

313 'CAT3': 'CAT III', 

314 'OUT': u'> CAT III', 

315 # Add more entries for other columns 

316 } 

317 

318 # Rename the columns of the DataFrame using the dictionary 

319 df.rename(columns=rename_columns, inplace=True) 

320 

321 # Set 'ROOM' column as the index 

322 df.set_index('ROOM', inplace=True) 

323 row_sums = df.sum(axis=1) 

324 # Create a new DataFrame by dividing the original DataFrame by the row 

325 # sums 

326 normalized_df = df.div(row_sums, axis=0) 

327 normalized_df = normalized_df * 100 

328 fig, ax = plt.subplots(figsize=(13.2 / INCH, 8 / INCH)) 

329 x_pos = np.arange(len(normalized_df.index)) 

330 bar_width = 0.35 

331 bottom = np.zeros(len(normalized_df.index)) 

332 

333 for i, col in enumerate(normalized_df.columns): 

334 ax.bar(x_pos, normalized_df[col], width=bar_width, label=col, 

335 bottom=bottom) 

336 bottom += normalized_df[col] 

337 

338 ax.set_ylabel(u'% of hours per category') 

339 # plt.xticks(x_pos, df.index) 

340 plt.xticks([]) 

341 plt.ylim([0, 100]) 

342 lgnd = plt.legend(framealpha=0.0, ncol=1, 

343 prop={'size': 6}, bbox_to_anchor=[0.5, -0.5], 

344 loc="center", 

345 ncols=4) 

346 formatted_df = normalized_df.map(lambda x: f'{x:.0f}'+u'%') 

347 # Create a table below the bar plot with column names as row labels 

348 cell_text = [] 

349 for column in formatted_df.columns: 

350 cell_text.append(formatted_df[column]) 

351 

352 # Transpose the DataFrame for the table 

353 table = plt.table(cellText=cell_text, rowLabels=formatted_df.columns, 

354 colLabels=formatted_df.index, 

355 cellLoc='center', 

356 loc='bottom') 

357 table.auto_set_font_size(False) 

358 table.set_fontsize(7) 

359 table.scale(1.0, 1.2) # Adjust the table size as needed 

360 plt.tight_layout() 

361 plt.savefig(export_path / 'DIN_EN_16798_all_zones_bar_table.pdf', 

362 bbox_inches='tight', 

363 bbox_extra_artists=(lgnd, table)) 

364 

365 @staticmethod 

366 def visualize_calendar(calendar_df, export_path, year='', 

367 color_only=False, save=True, 

368 save_as='', 

369 construction='', skip_legend=False, 

370 add_title=False, figsize=[7.6, 8], zone_dict=None): 

371 

372 logger.info(f"Plot PMV calendar plot for zone {calendar_df.columns[0]}") 

373 def visualize(zone_dict): 

374 

375 fig, ax = plt.subplots(figsize=(figsize[0]/INCH, figsize[1]/INCH)) 

376 daily_mean = calendar_df.resample('D').mean() 

377 calendar_heatmap(ax, daily_mean, color_only) 

378 title_name = calendar_df.columns[0] 

379 for key, item in zone_dict.items(): 

380 if key in title_name: 

381 title_name = title_name.replace(key, item) 

382 if add_title: 

383 plt.title(str(year) + ' ' + title_name) 

384 if save: 

385 plt.savefig(export_path / str(construction + 

386 save_as + title_name 

387 + '.pdf'), 

388 bbox_inches='tight') 

389 if skip_legend: 

390 plt.savefig(export_path / 'subplots' / str(construction + 

391 save_as + title_name 

392 + '.pdf'), 

393 bbox_inches='tight') 

394 plt.draw() 

395 plt.close() 

396 

397 def calendar_array(dates, data): 

398 i, j = zip(*[(d.day, d.month) for d in dates]) 

399 i = np.array(i) - min(i) 

400 j = np.array(j) - 1 

401 ni = max(i) + 1 

402 calendar = np.empty([ni, 12])#, dtype='S10') 

403 calendar[:] = np.nan 

404 calendar[i, j] = data 

405 return i, j, calendar 

406 

407 def calendar_heatmap(ax, df, color_only): 

408 

409 color_schema = ['#0232c2', '#028cc2', '#03ffff', 

410 '#02c248', '#bbc202', '#c27f02'] 

411 # Labels and their corresponding indices 

412 labels = ['-3 to -2', '-2 to -1', '-1 to 0', 

413 '0 to 1', '1 to 2', '2 to 3'] 

414 label_indices = np.arange(len(labels)+1) - 3 

415 

416 # Create a ListedColormap from the color schema 

417 cmap = ListedColormap(color_schema) 

418 df_dates = df.index 

419 df_data = df[df.columns[0]].values 

420 norm = Normalize(vmin=-3, vmax=3) 

421 

422 i, j, calendar = calendar_array(df_dates, df_data) 

423 

424 im = ax.imshow(calendar, aspect='auto', interpolation='none', 

425 cmap=cmap, norm=norm) 

426 label_days(ax, df_dates, i, j, calendar) 

427 if not color_only: 

428 label_data(ax, calendar) 

429 label_months(ax, df_dates, i, j, calendar) 

430 if not skip_legend: 

431 cbar = ax.figure.colorbar(im, ticks=label_indices) 

432 # Minor ticks 

433 ax.set_xticks(np.arange(-.5, len(calendar[0]), 1), minor=True) 

434 ax.set_yticks(np.arange(-.5, len(calendar[:,0]), 1), minor=True) 

435 

436 ax.grid(False) 

437 # Gridlines based on minor ticks 

438 ax.grid(which='minor', color='w', linestyle='-', linewidth=0.5) 

439 

440 # Remove minor ticks 

441 ax.tick_params(which='minor', bottom=False, left=False) # ax.get_yaxis().set_ticks(label_indices) 

442 # ax.get_yaxis().set_ticklabels(labels) 

443 

444 def label_data(ax, calendar): 

445 for (i, j), data in np.ndenumerate(calendar): 

446 if type(data) == str: 

447 ax.text(j, i, data, ha='center', va='center') 

448 elif np.isfinite(data): 

449 ax.text(j, i, round(data,1), ha='center', va='center') 

450 

451 def label_days(ax, dates, i, j, calendar): 

452 ni, nj = calendar.shape 

453 day_of_month = np.nan * np.zeros((ni, nj)) 

454 day_of_month[i, j] = [d.day for d in dates] 

455 

456 yticks = np.arange(31) 

457 yticklabels = [i+1 for i in yticks] 

458 ax.set_yticks(yticks) 

459 ax.set_yticklabels(yticklabels, fontsize=6) 

460 # ax.set(yticks=yticks, 

461 # yticklabels=yticklabels) 

462 

463 

464 def label_months(ax, dates, i, j, calendar): 

465 month_labels = np.array(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 

466 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']) 

467 months = np.array([d.month for d in dates]) 

468 uniq_months = sorted(set(months)) 

469 # xticks = [i[months == m].mean() for m in uniq_months] 

470 xticks = [i-1 for i in uniq_months] 

471 labels = [month_labels[m - 1] for m in uniq_months] 

472 ax.set(xticks=xticks) 

473 ax.set_xticklabels(labels, fontsize=6, rotation=90) 

474 ax.xaxis.tick_top() 

475 visualize(zone_dict)