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

287 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +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 storey_guid = ele.storeys[0] 

339 else: 

340 storey_guid = ele.storeys[0].guid 

341 space_area = ele.net_area 

342 

343 if not storey_guid or not space_area: 

344 self.logger.warning( 

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

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

347 f"ignored for floor plan plots. ") 

348 continue 

349 # Ignore very small areas 

350 min_area = min_space_area * ureg.m ** 2 

351 if space_area < min_area: 

352 self.logger.warning( 

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

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

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

356 continue 

357 

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

359 "space_data", {}) 

360 if area_specific: 

361 val = col_data.max() / space_area 

362 else: 

363 val = col_data.max() 

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

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

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

367 if "storey_min_value" in svg_adjust_dict[storey_guid] \ 

368 else val 

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

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

371 if "storey_max_value" in svg_adjust_dict[storey_guid] \ 

372 else val 

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

374 'text': val} 

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

376 # extraction to have all values for all spaces 

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

378 storey_min = storey_data["storey_min_value"] 

379 storey_max = storey_data["storey_max_value"] 

380 

381 # set common human-readable units 

382 common_unit = storey_min.to_compact().u 

383 storey_min = storey_min.to(common_unit) 

384 storey_max = storey_max.to(common_unit) 

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

386 if storey_min == storey_max: 

387 storey_min -= 1 * storey_min.u 

388 storey_max += 1 * storey_max.u 

389 

390 cmap = self.create_color_mapping( 

391 storey_min, 

392 storey_max, 

393 storey_med, 

394 plot_path, 

395 storey_guid, 

396 ) 

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

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

399 if storey_min == storey_max: 

400 storey_min -= 1 * storey_min.u 

401 storey_max += 1 * storey_max.u 

402 space_data['color'] = "red" 

403 else: 

404 space_data['color'] = ( 

405 self.get_color_for_value( 

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

407 # store value as text for floor plan plotting 

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

409 

410 # delete storey_min_value and storey_max_value as no longer needed 

411 for entry in svg_adjust_dict.values(): 

412 if 'storey_max_value' in entry: 

413 entry.pop('storey_max_value') 

414 if 'storey_min_value' in entry: 

415 entry.pop('storey_min_value') 

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

417 # *_modified svg plots. 

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

419 # json.dump(svg_adjust_dict, file) 

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

421 create_svg_floor_plan_plot(ifc_file, plot_path, svg_adjust_dict, 

422 result_str) 

423 

424 @staticmethod 

425 def create_color_mapping( 

426 min_val: float, max_val: float, med_val: float, 

427 sim_results_path: Path, storey_guid: str): 

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

429 

430 Args: 

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

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

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

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

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

436 

437 Returns: 

438 LinearSegmentedColormap: Created colormap object. 

439 """ 

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

441 if min_val == max_val: 

442 colors = ["red", "red", "red" ] 

443 else: 

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

445 cmap = LinearSegmentedColormap.from_list( 

446 'custom', colors) 

447 

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

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

450 

451 # Create a ScalarMappable to use the colormap 

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

453 sm.set_array([]) 

454 

455 # Create a color bar to display the colormap 

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

457 fig.subplots_adjust(bottom=0.5) 

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

459 

460 # set ticks and tick labels 

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

462 cbar.set_ticklabels( 

463 [ 

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

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

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

467 ]) 

468 # convert all values to common_unit 

469 

470 # Save the figure as an SVG file 

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

472 , format='svg') 

473 plt.close(fig) 

474 return cmap 

475 

476 @staticmethod 

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

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

479 

480 Args: 

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

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

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

484 cmap (LinearSegmentedColormap): Colormap object. 

485 

486 Returns: 

487 str: Hexadecimal representation of the color corresponding to the 

488 value. 

489 """ 

490 # Normalize the value between 0 and 1 

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

492 

493 # Get the color corresponding to the normalized value 

494 color = cmap(normalized_value) 

495 

496 return to_hex(color, keep_alpha=False) 

497 

498 

499 @staticmethod 

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

501 save_path: Optional[Path] = None, 

502 logo: bool = True, 

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

504 dpi: int = 300) -> None: 

505 """ 

506 Plot temperatures. 

507 

508 """ 

509 save_path_demand = (save_path / 

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

511 y_values = df[data] 

512 color = cm.RWTHBlau.p(100) 

513 

514 label_pad = 5 

515 # Create a new figure with specified size 

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

517 

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

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

520 

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

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

523 

524 plt.ylabel( 

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

526 labelpad=label_pad) 

527 # Smooth the data for better visibility 

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

529 # take values without units only for plot 

530 y_values = y_values.pint.magnitude 

531 

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

533 # Plotting the data 

534 plt.plot(y_values.index, 

535 y_values, color=color, 

536 linewidth=1, linestyle='-') 

537 

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

539 to_timestamp()) 

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

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

542 

543 # Rotate the tick labels for better visibility 

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

545 

546 # Limits 

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

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

549 # Adding x label 

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

551 # Add title 

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

553 # Add grid 

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

555 

556 # add bim2sim logo to plot 

557 if logo: 

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

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

560 fig_size[1] * 0.95 * dpi] 

561 PlotBEPSResults.add_logo(dpi, fig_size, logo_pos) 

562 

563 # Show or save the plot 

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

565 

566 @staticmethod 

567 def plot_multiple_temperatures( 

568 df: pd.DataFrame, save_path: Optional[Path] = None, logo: bool = 

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

570 dpi: int = 300) -> None: 

571 """ 

572 Plot multiple temperature series in one plot. 

573 

574 Parameters: 

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

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

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

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

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

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

581 """ 

582 rename_surfs_in_space = \ 

583 { 

584 "3hiy47ppf5B8MyZqbpTfpc": 

585 { 

586 "14KVjb4bn2zOxEfCwOWKb_": "Floor", 

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

588 "174xOdW7H4iw488r9lVn33": "Roof", 

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

590 "18dtnPzhbDfA93nuFIFIQD": "InnerWall North", 

591 "3eJjh1rC9D6u9xQvTJvVV1": "OuterWall South", 

592 "3YwbLt4uL17hwYjrGkpnG2": "Window South", 

593 "1T2XvptoT41wqhpFAnaD97": "OuterWall East", 

594 "3tw6evsPf4cOKvenfxmdSF": "Window East" 

595 }, 

596 } 

597 

598 key_match = False 

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

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

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

602 key_match = True 

603 if key_match: 

604 rename_surfs_mapping = {k: v for inner_dict in 

605 rename_surfs_in_space.values() for 

606 k, v in inner_dict.items()} 

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

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

609 df.columns for key in rename_surfs_mapping 

610 if key in col} 

611 

612 # Rename the columns 

613 df = df.rename(columns=columns_to_rename) 

614 

615 # Drop columns that were not renamed 

616 df = df[columns_to_rename.values()] 

617 

618 save_path_demand = ( 

619 save_path / "surface_temperatures.svg") if save_path else \ 

620 None 

621 label_pad = 5 

622 

623 # Create a new figure with specified size 

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

625 

626 # Get a colormap with enough colors for all columns 

627 

628 # Iterate over each column in the DataFrame 

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

630 y_values = df[column] 

631 

632 # Escape underscores in column names for LaTeX formatting 

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

634 

635 # Plot the data 

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

637 linewidth=1, 

638 linestyle='-') 

639 

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

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

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

643 

644 # Rotate the tick labels for better visibility 

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

646 

647 # Limits 

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

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

650 

651 # Adding labels and title 

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

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

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

655 

656 # Add grid 

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

658 

659 # Add legend below the x-axis 

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

661 bbox_to_anchor=(0.5, -0.25), 

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

663 

664 # Add bim2sim logo to plot 

665 # if logo: 

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

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

668 

669 # Save the plot if a path is provided 

670 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, format='svg') 

671 

672 

673 @staticmethod 

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

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

676 legend_title: str="", x_axis_title: str="", 

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

678 rename_columns_dict: dict= {}, 

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

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

681 """ 

682 Plot data from dataframe in one plot. 

683 

684 Parameters: 

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

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

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

688 plot_title (str): title of the plot. 

689 legend_title (str): title of legend. 

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

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

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

693 rename_columns_dict (dict): Dictionary to rename columns 

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

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

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

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

698 """ 

699 key_match = False 

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

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

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

703 key_match = True 

704 if key_match: 

705 rename_columns_mapping = {k: v for inner_dict in 

706 rename_columns_dict.values() for 

707 k, v in inner_dict.items()} 

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

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

710 df.columns for key in rename_columns_mapping 

711 if key in col} 

712 

713 # Rename the columns 

714 df = df.rename(columns=columns_to_rename) 

715 

716 # Drop columns that were not renamed 

717 df = df[columns_to_rename.values()] 

718 

719 save_path_demand = ( 

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

721 None 

722 label_pad = 5 

723 

724 # Create a new figure with specified size 

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

726 

727 # Get a colormap with enough colors for all columns 

728 # Iterate over each column in the DataFrame 

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

730 y_values = df[column] 

731 

732 # Escape underscores in column names for LaTeX formatting 

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

734 

735 # Plot the data 

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

737 linewidth=1, 

738 linestyle='-') 

739 

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

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

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

743 

744 # Rotate the tick labels for better visibility 

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

746 

747 # Limits 

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

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

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

751 

752 # Adding labels and title 

753 plt.xlabel(x_axis_title, labelpad=label_pad) 

754 plt.ylabel(y_axis_title, labelpad=label_pad) 

755 plt.title(plot_title, pad=20) 

756 

757 # Add grid 

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

759 

760 # Add legend below the x-axis 

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

762 bbox_to_anchor=(0.5, -0.25), 

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

764 

765 # Add bim2sim logo to plot 

766 # if logo: 

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

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

769 

770 # Save the plot if a path is provided 

771 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, 

772 format=file_type) 

773 def plot_thermal_discomfort(self): 

774 # TODO 

775 pass 

776 

777 # @staticmethod 

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

779 # # TODO: this is not completed yet 

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

781 # # Load the logo 

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

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

784 # # todo get rid of PIL package 

785 # logo = Image.open(logo_path) 

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

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

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

789 # # finished: 

790 # # # Calculate the desired scale factor 

791 # # scale_factor = 0.01 # Adjust as needed 

792 # # 

793 # # # Load the logo 

794 # # logo = plt.imread(logo_path) 

795 # # 

796 # # # Create an OffsetImage 

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

798 # # 

799 # # # Set the position of the image 

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

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

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