Coverage for bim2sim/tasks/bps/plot_results.py: 15%

291 statements  

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

1from typing import Optional, Tuple, List 

2 

3import matplotlib as mpl 

4from matplotlib import pyplot as plt 

5from matplotlib.colors import LinearSegmentedColormap, to_hex 

6from pathlib import Path 

7from PIL import Image 

8from RWTHColors import ColorManager 

9import pandas as pd 

10# scienceplots is marked as not used but is mandatory 

11import scienceplots 

12from matplotlib.dates import DateFormatter 

13 

14from bim2sim.kernel.ifc_file import IfcFileClass 

15from bim2sim.tasks.base import ITask 

16from bim2sim.elements.mapping.units import ureg 

17from bim2sim.elements.base_elements import SerializedElement 

18from bim2sim.utilities.svg_utils import create_svg_floor_plan_plot 

19 

20cm = ColorManager() 

21plt.rcParams.update(mpl.rcParamsDefault) 

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

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

24 

25# Update rcParams for font settings 

26plt.rcParams.update({ 

27 'font.size': 20, 

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

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

30 'legend.frameon': True, 

31 'legend.facecolor': 'white', 

32 'legend.framealpha': 0.5, 

33 'legend.edgecolor': 'black', 

34 "lines.linewidth": 0.4, 

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

36 "pgf.rcfonts": True, 

37}) 

38 

39 

40class PlotBEPSResults(ITask): 

41 """Class for plotting results of BEPS. 

42 

43 This class provides methods to create various plots including time series 

44 and bar charts for energy consumption and temperatures. 

45 """ 

46 

47 reads = ('df_finals', 'sim_results_path', 'ifc_files', 'elements') 

48 final = True 

49 

50 def run(self, df_finals: dict, sim_results_path: Path, 

51 ifc_files: List[Path], elements: dict) -> None: 

52 """ 

53 Run the plotting process for BEPS results. 

54 

55 Args: 

56 df_finals (dict): Dictionary of DataFrames containing final 

57 simulation results. 

58 sim_results_path (Path): Path to save simulation results. 

59 ifc_files (List[Path]): List of IFC file paths. 

60 elements (dict): Dictionary of building elements. 

61 """ 

62 if not self.playground.sim_settings.create_plots: 

63 self.logger.warning("Skipping task PlotBEPSResults as sim_setting " 

64 "'create_plots' is set to False.") 

65 return 

66 

67 plugin_name = self.playground.project.plugin_cls.name 

68 if (plugin_name == 'TEASER' and not 

69 self.playground.sim_settings.dymola_simulation): 

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

71 " 'dymola_simulation' is set to False and no" 

72 " simulation was performed.") 

73 return 

74 

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

76 plot_path = sim_results_path / bldg_name / "plots" 

77 plot_path.mkdir(exist_ok=True) 

78 for ifc_file in ifc_files: 

79 self.plot_floor_plan_with_results( 

80 df, elements, 'heat_demand_rooms', 

81 ifc_file, plot_path, area_specific=True) 

82 self.plot_total_consumption(df, plot_path) 

83 if (any(df.filter(like='surf_inside_temp')) and 

84 self.playground.sim_settings.plot_singe_zone_guid): 

85 self.plot_multiple_temperatures(df.filter( 

86 like='surf_inside_temp'), plot_path,logo=False) 

87 

88 def plot_total_consumption( 

89 self, df: pd.DataFrame, plot_path: Path) -> None: 

90 """ 

91 Plot total consumption for heating and cooling. 

92 

93 Args: 

94 df (pd.DataFrame): DataFrame containing consumption data. 

95 plot_path (Path): Path to save the plots. 

96 """ 

97 self.plot_demands_time_series(df, ["Heating"], plot_path, logo=False, 

98 title=None) 

99 self.plot_demands_time_series(df, ["Cooling"], plot_path, logo=False, 

100 title=None) 

101 self.plot_demands_time_series(df, ["Heating", "Cooling"], plot_path, 

102 window=24, logo=False) 

103 self.plot_temperatures(df, "air_temp_out", plot_path, logo=False) 

104 self.plot_demands_bar(df, plot_path, logo=False, title=None) 

105 

106 @staticmethod 

107 def plot_demands_time_series(df: pd.DataFrame, demand_type: List[str], 

108 save_path: Optional[Path] = None, 

109 logo: bool = True, total_label: bool = True, 

110 window: int = 12, 

111 fig_size: Tuple[int, int] = (10, 6), 

112 dpi: int = 300, title: Optional[str] = None) -> None: 

113 """ 

114 Plot time series of energy demands. 

115 

116 Args: 

117 df (pd.DataFrame): DataFrame containing demand data. 

118 demand_type (List[str]): List of demand types to plot ("Heating" and/or "Cooling"). 

119 save_path (Optional[Path]): Path to save the plot. If None, the plot will be displayed. 

120 logo (bool): Whether to add a logo to the plot. 

121 total_label (bool): Whether to add total energy labels to the legend. 

122 window (int): Window size for rolling mean calculation. 

123 fig_size (Tuple[int, int]): Figure size in inches. 

124 dpi (int): Dots per inch for the figure. 

125 title (Optional[str]): Title of the plot. 

126 """ 

127 fig, ax = plt.subplots(figsize=fig_size, dpi=dpi) 

128 

129 total_energies = {} 

130 colors = {'heating': cm.RWTHRot.p(100), 'cooling': cm.RWTHBlau.p(100)} 

131 

132 for dt in demand_type: 

133 if dt.lower() == "heating": 

134 y_values = df["heat_demand_total"] 

135 total_energy_col = "heat_energy_total" 

136 label = "Heating" 

137 elif dt.lower() == "cooling": 

138 y_values = df["cool_demand_total"] 

139 total_energy_col = "cool_energy_total" 

140 label = "Cooling" 

141 else: 

142 raise ValueError(f"Demand type {dt} is not supported.") 

143 

144 total_energy = df[total_energy_col].sum() 

145 total_energies[label] = total_energy 

146 

147 if y_values.pint.magnitude.max() > 5000: 

148 y_values = y_values.pint.to(ureg.kilowatt) 

149 ax.set_ylabel(f"Demand / {format(y_values.pint.units, '~')}", labelpad=5) 

150 

151 y_values = y_values.rolling(window=window).mean() 

152 ax.plot(y_values.index, y_values, color=colors[dt.lower()], 

153 linewidth=1.5, linestyle='-', label=f"{label} Demand") 

154 

155 first_day_of_months = (y_values.index.to_period('M').unique(). 

156 to_timestamp()) 

157 ax.set_xticks(first_day_of_months) 

158 ax.set_xticklabels([month.strftime('%b') 

159 for month in first_day_of_months]) 

160 plt.gcf().autofmt_xdate(rotation=45) 

161 

162 ax.set_xlim(y_values.index[0], y_values.index[-1]) 

163 if title: 

164 ax.set_title(title, pad=20) 

165 ax.grid(True, linestyle='--', alpha=0.6) 

166 ax.spines['top'].set_visible(False) 

167 ax.spines['right'].set_visible(False) 

168 

169 if total_label: 

170 legend_labels = [f"{label} Total energy: {format(round(total_energies[label].to(ureg.megawatt_hour), 2), '~')}" for label in total_energies] 

171 handles, labels = ax.get_legend_handles_labels() 

172 leg = ax.legend(handles, legend_labels + labels, 

173 loc='upper right', framealpha=0.9, 

174 facecolor='white', edgecolor='black') 

175 leg.get_frame().set_facecolor('white') 

176 leg.get_frame().set_alpha(0.9) 

177 leg.get_frame().set_edgecolor('black') 

178 

179 if logo: 

180 raise Warning("Logo option is currently not supported") 

181 

182 if save_path: 

183 save_path_demand = save_path / "demands_combined.pdf" 

184 PlotBEPSResults.save_or_show_plot( 

185 save_path_demand, dpi, format='pdf') 

186 else: 

187 plt.show() 

188 

189 @staticmethod 

190 def plot_demands_bar(df: pd.DataFrame, 

191 save_path: Optional[Path] = None, 

192 logo: bool = True, total_label: bool = True, 

193 fig_size: Tuple[int, int] = (10, 6), 

194 dpi: int = 300, title: Optional[str] = None) -> None: 

195 """ 

196 Plot monthly energy consumption as bar chart. 

197 

198 Args: 

199 df (pd.DataFrame): DataFrame containing energy consumption data. 

200 save_path (Optional[Path]): Path to save the plot. If None, the 

201 plot will be displayed. 

202 logo (bool): Whether to add a logo to the plot. 

203 total_label (bool): Whether to add total energy labels to the 

204 legend. 

205 fig_size (Tuple[int, int]): Figure size in inches. 

206 dpi (int): Dots per inch for the figure. 

207 title (Optional[str]): Title of the plot. 

208 """ 

209 save_path_monthly = save_path / "monthly_energy_consumption.pdf" if\ 

210 save_path else None 

211 label_pad = 5 

212 df_copy = df.copy() 

213 df_copy.index = pd.to_datetime(df_copy.index, format='%m/%d-%H:%M:%S') 

214 

215 df_copy['hourly_heat_energy'] = df_copy['heat_energy_total'].pint.to( 

216 ureg.kilowatthours) 

217 df_copy['hourly_cool_energy'] = df_copy['cool_energy_total'].pint.to( 

218 ureg.kilowatthours) 

219 

220 monthly_sum_heat = df_copy['hourly_heat_energy'].groupby( 

221 df_copy.index.to_period('M')).sum() 

222 monthly_sum_cool = df_copy['hourly_cool_energy'].groupby( 

223 df_copy.index.to_period('M')).sum() 

224 

225 monthly_labels = monthly_sum_heat.index.strftime('%b').tolist() 

226 monthly_sum_heat = [q.magnitude for q in monthly_sum_heat] 

227 monthly_sum_cool = [q.magnitude for q in monthly_sum_cool] 

228 

229 fig, ax = plt.subplots(figsize=fig_size, dpi=dpi) 

230 

231 bar_width = 0.4 

232 index = range(len(monthly_labels)) 

233 

234 ax.bar(index, monthly_sum_heat, color=cm.RWTHRot.p(100), 

235 width=bar_width, label='Heating') 

236 ax.bar([p + bar_width for p in index], monthly_sum_cool, 

237 color=cm.RWTHBlau.p(100), width=bar_width, label='Cooling') 

238 

239 ax.set_ylabel(f"Energy Consumption / " 

240 f"{format(df_copy['hourly_cool_energy'].pint.units,'~')}" 

241 f"", labelpad=label_pad) 

242 if title: 

243 ax.set_title(title, pad=20) 

244 ax.set_xticks([p + bar_width / 2 for p in index]) 

245 ax.set_xticklabels(monthly_labels, rotation=45) 

246 

247 ax.grid(True, linestyle='--', alpha=0.6) 

248 ax.spines['top'].set_visible(False) 

249 ax.spines['right'].set_visible(False) 

250 

251 ax.legend(frameon=True, loc='upper right', edgecolor='black') 

252 

253 if logo: 

254 logo_pos = [fig_size[0] * dpi * 0.005, fig_size[1] * 0.95 * dpi] 

255 PlotBEPSResults.add_logo(dpi, fig_size, logo_pos) 

256 

257 PlotBEPSResults.save_or_show_plot(save_path_monthly, dpi, format='pdf') 

258 

259 @staticmethod 

260 def save_or_show_plot(save_path: Optional[Path], dpi: int, 

261 format: str = 'pdf') -> None: 

262 """ 

263 Save or show the plot depending on whether a save path is provided. 

264 

265 Args: 

266 save_path (Optional[Path]): Path to save the plot. If None, the 

267 plot will be displayed. 

268 dpi (int): Dots per inch for the saved figure. 

269 format (str): Format to save the figure in. 

270 """ 

271 if save_path: 

272 plt.ioff() 

273 plt.savefig(save_path, dpi=dpi, format=format) 

274 else: 

275 plt.show() 

276 

277 def plot_floor_plan_with_results( 

278 self, df: pd.DataFrame, 

279 elements, 

280 result_str, 

281 ifc_file: IfcFileClass, 

282 plot_path: Path, 

283 min_space_area: float = 2, 

284 area_specific: bool = True 

285 ): 

286 """Plot a floor plan colorized based on specific heat demand. 

287 

288 The plot colors each room based on the specific heat demand, while blue 

289 is the color for minimum heat demand and red for maximum. 

290 

291 Args: 

292 df (DataFrame): The DataFrame containing sim result data. 

293 elements (dict[guid: element]): dict hat holds bim2sim elements 

294 result_str (str): one of sim_results settings that should be 

295 plotted. Currently, always max() of this is plotted. 

296 ifc_file (IfcFileClass): bim2sim IfcFileClass object. 

297 plot_path (Path): Path to store simulation results. 

298 min_space_area (float): minimal area in m² of a space that should 

299 be taken into account for the result calculation in the plot. 

300 area_specific (bool): True if result_str values should be divided 

301 by area to get valuer per square meter. 

302 

303 TODO: this is currently not working for aggregated zones. 

304 Combined zones, how to proceed: 

305 - All rooms in the combined zone are given the same color and 

306 the same value 

307 - Rooms need names in the plot 

308 - Legend in the margin showing which room name belongs to which 

309 zone 

310 

311 

312 Generally revise: 

313 - Unit in the color mapping plot and in the plot for numerical 

314 values 

315 """ 

316 # check if result_str is valid for floor plan visualization 

317 if result_str not in self.playground.sim_settings.sim_results: 

318 raise ValueError(f'Result {result_str} was not requested by ' 

319 f'sim_setting "sim_results" or is not provided' 

320 f'by the simulation. ' 

321 f'Please Check your "sim_results" settings.') 

322 if "_rooms" not in result_str: 

323 raise ValueError(f'Result {result_str} does not provide room level' 

324 f'information. Floor plan visualization is only ' 

325 f'available for room level results.') 

326 # create the dict with all space guids and resulting values in the 

327 # first run 

328 svg_adjust_dict = {} 

329 for col_name, col_data in df.items(): 

330 if result_str + '_' in col_name and 'total' not in col_name: 

331 space_guid = col_name.split(result_str + '_')[-1] 

332 storey_guid = None 

333 space_area = None 

334 for guid, ele in elements.items(): 

335 if guid == space_guid: 

336 # TODO use all storeys for aggregated zones 

337 if isinstance(ele, SerializedElement): 

338 if isinstance(ele.storeys[0], str): 

339 storey_guid = ele.storeys[0] 

340 else: 

341 storey_guid = ele.storeys[0].guid 

342 else: 

343 storey_guid = ele.storeys[0].guid 

344 space_area = ele.net_area 

345 

346 if not storey_guid or not space_area: 

347 self.logger.warning( 

348 f"For space with guid {space_guid} no" 

349 f" fitting storey could be found. This space will be " 

350 f"ignored for floor plan plots. ") 

351 continue 

352 # Ignore very small areas 

353 min_area = min_space_area * ureg.m ** 2 

354 if space_area < min_area: 

355 self.logger.warning( 

356 f"Space with guid {space_guid} is smaller than " 

357 f"the minimal threshold area of {min_area}. The " 

358 f"space is ignored for floor plan plotting. ") 

359 continue 

360 

361 svg_adjust_dict.setdefault(storey_guid, {}).setdefault( 

362 "space_data", {}) 

363 if area_specific: 

364 val = col_data.max() / space_area 

365 else: 

366 val = col_data.max() 

367 # update minimal and maximal value to get a useful color scale 

368 svg_adjust_dict[storey_guid]["storey_min_value"] = min( 

369 val, svg_adjust_dict[storey_guid]["storey_min_value"]) \ 

370 if "storey_min_value" in svg_adjust_dict[storey_guid] \ 

371 else val 

372 svg_adjust_dict[storey_guid]["storey_max_value"] = max( 

373 val, svg_adjust_dict[storey_guid]["storey_max_value"]) \ 

374 if "storey_max_value" in svg_adjust_dict[storey_guid] \ 

375 else val 

376 svg_adjust_dict[storey_guid]["space_data"][space_guid] = { 

377 'text': val} 

378 # create the color mapping, this needs to be done after the value 

379 # extraction to have all values for all spaces 

380 for storey_guid, storey_data in svg_adjust_dict.items(): 

381 storey_min = storey_data["storey_min_value"] 

382 storey_max = storey_data["storey_max_value"] 

383 

384 # set common human-readable units 

385 common_unit = storey_min.to_compact().u 

386 storey_min = storey_min.to(common_unit) 

387 storey_max = storey_max.to(common_unit) 

388 storey_med = round((storey_min + storey_max) / 2, 1).to(common_unit) 

389 if storey_min == storey_max: 

390 storey_min -= 1 * storey_min.u 

391 storey_max += 1 * storey_max.u 

392 

393 cmap = self.create_color_mapping( 

394 storey_min, 

395 storey_max, 

396 storey_med, 

397 plot_path, 

398 storey_guid, 

399 ) 

400 for space_guid, space_data in storey_data["space_data"].items(): 

401 value = space_data["text"].to(common_unit) 

402 if storey_min == storey_max: 

403 storey_min -= 1 * storey_min.u 

404 storey_max += 1 * storey_max.u 

405 space_data['color'] = "red" 

406 else: 

407 space_data['color'] = ( 

408 self.get_color_for_value( 

409 value.m, storey_min.m, storey_max.m, cmap)) 

410 # store value as text for floor plan plotting 

411 space_data['text'] = str(value.m.round(1)) 

412 

413 # delete storey_min_value and storey_max_value as no longer needed 

414 for entry in svg_adjust_dict.values(): 

415 if 'storey_max_value' in entry: 

416 entry.pop('storey_max_value') 

417 if 'storey_min_value' in entry: 

418 entry.pop('storey_min_value') 

419 # TODO merge the create color_mapping.svg into each of the created 

420 # *_modified svg plots. 

421 # with open("svg_adjust_dict.json", 'w') as file: 

422 # json.dump(svg_adjust_dict, file) 

423 # TODO cleanup temp files of color mapping and so on 

424 create_svg_floor_plan_plot(ifc_file, plot_path, svg_adjust_dict, 

425 result_str) 

426 

427 @staticmethod 

428 def create_color_mapping( 

429 min_val: float, max_val: float, med_val: float, 

430 sim_results_path: Path, storey_guid: str): 

431 """Create a colormap from blue to red and save it as an SVG file. 

432 

433 Args: 

434 min_val (float): Minimum value for the colormap range. 

435 max_val (float): Maximum value for the colormap range. 

436 med_val (float): medium value for the colormap range. 

437 sim_results_path (Path): Path to the simulation results file. 

438 storey_guid (str): GUID of storey to create color mapping for. 

439 

440 Returns: 

441 LinearSegmentedColormap: Created colormap object. 

442 """ 

443 # if whole storey has only one or the same values color is static 

444 if min_val == max_val: 

445 colors = ["red", "red", "red" ] 

446 else: 

447 colors = ['blue', 'purple', 'red'] 

448 cmap = LinearSegmentedColormap.from_list( 

449 'custom', colors) 

450 

451 # Create a normalization function to map values between 0 and 1 

452 normalize = plt.Normalize(vmin=min_val.m, vmax=max_val.m) 

453 

454 # Create a ScalarMappable to use the colormap 

455 sm = plt.cm.ScalarMappable(cmap=cmap, norm=normalize) 

456 sm.set_array([]) 

457 

458 # Create a color bar to display the colormap 

459 fig, ax = plt.subplots(figsize=(0.5, 6)) 

460 fig.subplots_adjust(bottom=0.5) 

461 cbar = plt.colorbar(sm, orientation='vertical', cax=ax) 

462 

463 # set ticks and tick labels 

464 cbar.set_ticks([min_val.m, med_val.m, max_val.m]) 

465 cbar.set_ticklabels( 

466 [ 

467 f"${min_val.to_compact():.4~L}$", 

468 f"${med_val.to_compact():.4~L}$", 

469 f"${max_val.to_compact():.4~L}$", 

470 ]) 

471 # convert all values to common_unit 

472 

473 # Save the figure as an SVG file 

474 plt.savefig(sim_results_path / f'color_mapping_{storey_guid}.svg' 

475 , format='svg') 

476 plt.close(fig) 

477 return cmap 

478 

479 @staticmethod 

480 def get_color_for_value(value, min_val, max_val, cmap): 

481 """Get the color corresponding to a value within the given colormap. 

482 

483 Args: 

484 value (float): Value for which the corresponding color is requested. 

485 min_val (float): Minimum value of the colormap range. 

486 max_val (float): Maximum value of the colormap range. 

487 cmap (LinearSegmentedColormap): Colormap object. 

488 

489 Returns: 

490 str: Hexadecimal representation of the color corresponding to the 

491 value. 

492 """ 

493 # Normalize the value between 0 and 1 

494 normalized_value = (value - min_val) / (max_val - min_val) 

495 

496 # Get the color corresponding to the normalized value 

497 color = cmap(normalized_value) 

498 

499 return to_hex(color, keep_alpha=False) 

500 

501 

502 @staticmethod 

503 def plot_temperatures(df: pd.DataFrame, data: str, 

504 save_path: Optional[Path] = None, 

505 logo: bool = True, 

506 window: int = 12, fig_size: Tuple[int, int] = (10, 6), 

507 dpi: int = 300) -> None: 

508 """ 

509 Plot temperatures. 

510 

511 """ 

512 save_path_demand = (save_path / 

513 f"{data.lower()}.pdf") 

514 y_values = df[data] 

515 color = cm.RWTHBlau.p(100) 

516 

517 label_pad = 5 

518 # Create a new figure with specified size 

519 fig = plt.figure(figsize=fig_size, dpi=dpi) 

520 

521 # Define spaces next to the real plot with absolute values 

522 # fig.subplots_adjust(left=0.05, right=0.95, top=1.0, bottom=0.0) 

523 

524 # Determine if y-axis needs to be in kilowatts 

525 y_values = y_values.pint.to(ureg.degree_Celsius) 

526 

527 plt.ylabel( 

528 f"{data} / {format(y_values.pint.units, '~')}", 

529 labelpad=label_pad) 

530 # Smooth the data for better visibility 

531 # y_values = y_values.rolling(window=window).mean() 

532 # take values without units only for plot 

533 y_values = y_values.pint.magnitude 

534 

535 # y_values.index = pd.to_datetime(df.index, format='%m/%d-%H:%M:%S') 

536 # Plotting the data 

537 plt.plot(y_values.index, 

538 y_values, color=color, 

539 linewidth=1, linestyle='-') 

540 

541 first_day_of_months = (y_values.index.to_period('M').unique(). 

542 to_timestamp()) 

543 plt.xticks(first_day_of_months.strftime('%Y-%m-%d'), 

544 [month.strftime('%b') for month in first_day_of_months]) 

545 

546 # Rotate the tick labels for better visibility 

547 plt.gcf().autofmt_xdate(rotation=45) 

548 

549 # Limits 

550 plt.xlim(y_values.index[0], y_values.index[-1]) 

551 plt.ylim(y_values.min()*1.1, y_values.max() * 1.1) 

552 # Adding x label 

553 plt.xlabel("Time", labelpad=label_pad) 

554 # Add title 

555 plt.title(f"{data}", pad=20) 

556 # Add grid 

557 plt.grid(True, linestyle='--', alpha=0.6) 

558 

559 # add bim2sim logo to plot 

560 if logo: 

561 raise Warning("Logo option is currently not supported") 

562 logo_pos = [fig_size[0] * dpi * 0.005, 

563 fig_size[1] * 0.95 * dpi] 

564 PlotBEPSResults.add_logo(dpi, fig_size, logo_pos) 

565 

566 # Show or save the plot 

567 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, format='pdf') 

568 

569 @staticmethod 

570 def plot_multiple_temperatures( 

571 df: pd.DataFrame, rename_dict: dict = None, 

572 save_path: Optional[Path] = None, logo: bool = 

573 True, window: int = 12, fig_size: Tuple[int, int] = (10, 6), 

574 dpi: int = 300) -> None: 

575 """ 

576 Plot multiple temperature series in one plot. 

577 

578 Parameters: 

579 df (pd.DataFrame): DataFrame containing the temperature data to plot. 

580 save_path (Optional[Path]): Path to save the plot as a PDF file. 

581 logo (bool): Whether to include a logo in the plot. 

582 window (int): Rolling window size for smoothing the data. 

583 fig_size (Tuple[int, int]): Size of the figure. 

584 dpi (int): Dots per inch for the plot resolution. 

585 """ 

586 if rename_dict: 

587 rename_surfs_in_space = rename_dict 

588 else: 

589 rename_surfs_in_space = \ 

590 { 

591 "3hiy47ppf5B8MyZqbpTfpc": 

592 { 

593 "14KVjb4bn2zOxEfCwOWKb_": "Floor", 

594 "10NUX$CcjBcRRxCQJh2Suf": "InnerWall West", 

595 "174xOdW7H4iw488r9lVn33": "Roof", 

596 "0aGOY_OOT9fva8zh0hPM$t": "InnerDoor North", 

597 "18dtnPzhbDfA93nuFIFIQD": "InnerWall North", 

598 "3eJjh1rC9D6u9xQvTJvVV1": "OuterWall South", 

599 "3YwbLt4uL17hwYjrGkpnG2": "Window South", 

600 "1T2XvptoT41wqhpFAnaD97": "OuterWall East", 

601 "3tw6evsPf4cOKvenfxmdSF": "Window East" 

602 }, 

603 } 

604 

605 key_match = False 

606 for key, value in rename_surfs_in_space.items(): 

607 for key2, value2 in rename_surfs_in_space[key].items(): 

608 if any(df.filter(like=key2)): 

609 key_match = True 

610 if key_match: 

611 rename_surfs_mapping = {k: v for inner_dict in 

612 rename_surfs_in_space.values() for 

613 k, v in inner_dict.items()} 

614 # Identify and rename columns that match keys in the mapping 

615 columns_to_rename = {col: rename_surfs_mapping[key] for col in 

616 df.columns for key in rename_surfs_mapping 

617 if key in col} 

618 

619 # Rename the columns 

620 df = df.rename(columns=columns_to_rename) 

621 

622 # Drop columns that were not renamed 

623 df = df[columns_to_rename.values()] 

624 

625 save_path_demand = ( 

626 save_path / "surface_temperatures.pdf") if save_path else \ 

627 None 

628 label_pad = 5 

629 

630 # Create a new figure with specified size 

631 fig = plt.figure(figsize=fig_size, dpi=dpi) 

632 

633 # Get a colormap with enough colors for all columns 

634 

635 # Iterate over each column in the DataFrame 

636 for i, column in enumerate(df.columns): 

637 y_values = df[column] 

638 

639 # Escape underscores in column names for LaTeX formatting 

640 # safe_column_name = column.replace('_', r'\_') 

641 

642 # Plot the data 

643 plt.plot(df.index, y_values, label=column, 

644 linewidth=1, 

645 linestyle='-') 

646 

647 # Format the x-axis labels with dd/MM format 

648 date_format = DateFormatter('%d/%m') 

649 plt.gca().xaxis.set_major_formatter(date_format) 

650 

651 # Rotate the tick labels for better visibility 

652 plt.gcf().autofmt_xdate(rotation=45) 

653 

654 # Limits 

655 plt.xlim(df.index[0], df.index[-1]) 

656 plt.ylim(df.min().min() * 0.99, df.max().max() * 1.01) 

657 

658 # Adding labels and title 

659 plt.xlabel("Date", labelpad=label_pad) 

660 plt.ylabel("Temperature / \u00B0C", labelpad=label_pad) 

661 # plt.title("Surface Temperature Data", pad=20) 

662 

663 # Add grid 

664 plt.grid(True, linestyle='--', alpha=0.6) 

665 

666 # Add legend below the x-axis 

667 plt.legend(title="", loc='upper center', 

668 bbox_to_anchor=(0.5, -0.25), 

669 ncol=3, fontsize='small', frameon=False) 

670 

671 # Add bim2sim logo to plot 

672 # if logo: 

673 # logo_pos = [fig_size[0] * dpi * 0.005, fig_size[1] * 0.95 * dpi] 

674 # PlotBEPSResults.add_logo(dpi, fig_size, logo_pos) 

675 

676 # Save the plot if a path is provided 

677 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, format='pdf') 

678 

679 

680 @staticmethod 

681 def plot_dataframe(df: pd.DataFrame, save_path: Optional[Path] = None, 

682 file_name: str =None, plot_title: str="", 

683 legend_title: str="", x_axis_title: str="", 

684 y_axis_title: str = "", file_type="svg", 

685 rename_columns_dict: dict= {}, 

686 logo: bool = True, window: int = 12, fig_size: Tuple[ 

687 int, int] = (10, 6), dpi: int = 300) -> None: 

688 """ 

689 Plot data from dataframe in one plot. 

690 

691 Parameters: 

692 df (pd.DataFrame): DataFrame containing the data to plot. 

693 save_path (Optional[Path]): Path to save the plot as a PDF file. 

694 file_name (str): file name for the new plot 

695 plot_title (str): title of the plot. 

696 legend_title (str): title of legend. 

697 x_axis_title (str): title of x-axis. 

698 y_axis_title (str): title of y-axis. 

699 file_type (str): file type for figure export. Defaults to "svg" 

700 rename_columns_dict (dict): Dictionary to rename columns 

701 logo (bool): Whether to include a logo in the plot. 

702 window (int): Rolling window size for smoothing the data. 

703 fig_size (Tuple[int, int]): Size of the figure. 

704 dpi (int): Dots per inch for the plot resolution. 

705 """ 

706 key_match = False 

707 for key, value in rename_columns_dict.items(): 

708 for key2, value2 in rename_columns_dict[key].items(): 

709 if any(df.filter(like=key2)): 

710 key_match = True 

711 if key_match: 

712 rename_columns_mapping = {k: v for inner_dict in 

713 rename_columns_dict.values() for 

714 k, v in inner_dict.items()} 

715 # Identify and rename columns that match keys in the mapping 

716 columns_to_rename = {col: rename_columns_mapping[key] for col in 

717 df.columns for key in rename_columns_mapping 

718 if key in col} 

719 

720 # Rename the columns 

721 df = df.rename(columns=columns_to_rename) 

722 

723 # Drop columns that were not renamed 

724 df = df[columns_to_rename.values()] 

725 

726 save_path_demand = ( 

727 save_path / f"{file_name}.{file_type}") if save_path else \ 

728 None 

729 label_pad = 5 

730 

731 # Create a new figure with specified size 

732 fig = plt.figure(figsize=fig_size, dpi=dpi) 

733 

734 # Get a colormap with enough colors for all columns 

735 # Iterate over each column in the DataFrame 

736 for i, column in enumerate(df.columns): 

737 y_values = df[column] 

738 

739 # Escape underscores in column names for LaTeX formatting 

740 # safe_column_name = column.replace('_', r'\_') 

741 

742 # Plot the data 

743 plt.plot(df.index, y_values, label=column, 

744 linewidth=1, 

745 linestyle='-') 

746 

747 # Format the x-axis labels with dd/MM format 

748 date_format = DateFormatter('%d/%m') 

749 plt.gca().xaxis.set_major_formatter(date_format) 

750 

751 # Rotate the tick labels for better visibility 

752 plt.gcf().autofmt_xdate(rotation=45) 

753 

754 # Limits 

755 plt.xlim(df.index[0], df.index[-1]) 

756 plt.ylim(df.min().min() - abs(df.min().min())*0.02, df.max().max() + 

757 abs(df.max().max())*0.02) 

758 

759 # Adding labels and title 

760 plt.xlabel(x_axis_title, labelpad=label_pad) 

761 plt.ylabel(y_axis_title, labelpad=label_pad) 

762 plt.title(plot_title, pad=20) 

763 

764 # Add grid 

765 plt.grid(True, linestyle='--', alpha=0.6) 

766 

767 # Add legend below the x-axis 

768 plt.legend(title=legend_title, loc='upper center', 

769 bbox_to_anchor=(0.5, -0.25), 

770 ncol=3, fontsize='small', frameon=False) 

771 

772 # Add bim2sim logo to plot 

773 # if logo: 

774 # logo_pos = [fig_size[0] * dpi * 0.005, fig_size[1] * 0.95 * dpi] 

775 # PlotBEPSResults.add_logo(dpi, fig_size, logo_pos) 

776 

777 # Save the plot if a path is provided 

778 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, 

779 format=file_type) 

780 def plot_thermal_discomfort(self): 

781 # TODO 

782 pass 

783 

784 # @staticmethod 

785 # def add_logo(dpi, fig_size, logo_pos): 

786 # # TODO: this is not completed yet 

787 # """Adds the logo to the existing plot.""" 

788 # # Load the logo 

789 # logo_path = Path(bim2sim.__file__).parent.parent \ 

790 # / "docs/source/img/static/b2s_logo.png" 

791 # # todo get rid of PIL package 

792 # logo = Image.open(logo_path) 

793 # logo.thumbnail((fig_size[0] * dpi / 10, fig_size[0] * dpi / 10)) 

794 # plt.figimage(logo, xo=logo_pos[0], yo=logo_pos[1], alpha=1) 

795 # # TOdo resizing is not well done yet, this is an option but not 

796 # # finished: 

797 # # # Calculate the desired scale factor 

798 # # scale_factor = 0.01 # Adjust as needed 

799 # # 

800 # # # Load the logo 

801 # # logo = plt.imread(logo_path) 

802 # # 

803 # # # Create an OffsetImage 

804 # # img = OffsetImage(logo, zoom=scale_factor) 

805 # # 

806 # # # Set the position of the image 

807 # # ab = AnnotationBbox(img, (0.95, -0.1), frameon=False, 

808 # # xycoords='axes fraction', boxcoords="axes fraction") 

809 # # plt.gca().add_artist(ab)