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
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1import json
2import logging
3from pathlib import Path
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
12from bim2sim.tasks.bps import PlotBEPSResults
14INCH = 2.54
16logger = logging.getLogger(__name__)
17cm = ColorManager()
18plt.rcParams.update(mpl.rcParamsDefault)
19plt.style.use(['science', 'grid', 'rwth'])
20plt.style.use(['science', 'no-latex'])
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})
36class PlotComfortResults(PlotBEPSResults):
37 reads = ('df_finals', 'sim_results_path', 'ifc_files')
38 final = True
40 def run(self, df_finals, sim_results_path, ifc_files):
41 """Plots the results for BEPS simulations.
43 This holds pre configured functions to plot the results of the BEPS
44 simulations with the EnergyPlus-based PluginComfort .
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
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)
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)
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)
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
116 renamed_dict[key] = new_value
117 return renamed_dict
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
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")
135 def apply_en16798_to_all_zones(self, df, zone_dict, export_path):
136 """Generate EN 16798 diagrams for all thermal zones.
138 """
139 logger.info("Plot DIN EN 16798 diagrams for all zones ...")
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
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} ...")
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
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.
168 """
169 logger.info(f"Plot DIN EN 16798 diagrams for zone {guid}: {room_name}.")
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
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
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
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
210 lim_min = 10
211 lim_max = 30
213 ot = df['operative_air_temp_rooms_' + guid]
214 out_temp = df['site_outdoor_air_temp']
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])
239 analysis_file = export_path / 'DIN_EN_16798_analysis.csv'
240 cat_analysis_df.to_csv(analysis_file, mode='a+', header=False, sep=';')
242 plt.figure(figsize=(13.2 / INCH, 8.3 / INCH))
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')
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')
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'))
296 return cat_analysis_df
298 @staticmethod
299 def table_bar_plot_16798(df, export_path):
300 """Create bar plot with a table below for EN 16798 thermal comfort.
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.
306 """
307 # with columns: 'ROOM', 'CAT1', 'CAT2', 'CAT3', 'OUT'
308 logger.info(f"Plot DIN EN 16798 table bar plot all zones.")
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 }
318 # Rename the columns of the DataFrame using the dictionary
319 df.rename(columns=rename_columns, inplace=True)
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))
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]
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])
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))
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):
372 logger.info(f"Plot PMV calendar plot for zone {calendar_df.columns[0]}")
373 def visualize(zone_dict):
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()
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
407 def calendar_heatmap(ax, df, color_only):
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
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)
422 i, j, calendar = calendar_array(df_dates, df_data)
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)
436 ax.grid(False)
437 # Gridlines based on minor ticks
438 ax.grid(which='minor', color='w', linestyle='-', linewidth=0.5)
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)
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')
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]
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)
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)