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
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1from typing import Optional, Tuple, List
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
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
20cm = ColorManager()
21plt.rcParams.update(mpl.rcParamsDefault)
22plt.style.use(['science', 'grid', 'rwth'])
23plt.style.use(['science', 'no-latex'])
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})
40class PlotBEPSResults(ITask):
41 """Class for plotting results of BEPS.
43 This class provides methods to create various plots including time series
44 and bar charts for energy consumption and temperatures.
45 """
47 reads = ('df_finals', 'sim_results_path', 'ifc_files', 'elements')
48 final = True
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.
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
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
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)
88 def plot_total_consumption(
89 self, df: pd.DataFrame, plot_path: Path) -> None:
90 """
91 Plot total consumption for heating and cooling.
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)
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.
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)
129 total_energies = {}
130 colors = {'heating': cm.RWTHRot.p(100), 'cooling': cm.RWTHBlau.p(100)}
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.")
144 total_energy = df[total_energy_col].sum()
145 total_energies[label] = total_energy
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)
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")
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)
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)
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')
179 if logo:
180 raise Warning("Logo option is currently not supported")
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()
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.
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')
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)
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()
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]
229 fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
231 bar_width = 0.4
232 index = range(len(monthly_labels))
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')
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)
247 ax.grid(True, linestyle='--', alpha=0.6)
248 ax.spines['top'].set_visible(False)
249 ax.spines['right'].set_visible(False)
251 ax.legend(frameon=True, loc='upper right', edgecolor='black')
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)
257 PlotBEPSResults.save_or_show_plot(save_path_monthly, dpi, format='pdf')
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.
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()
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.
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.
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.
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
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
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
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"]
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
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))
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)
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.
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.
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)
448 # Create a normalization function to map values between 0 and 1
449 normalize = plt.Normalize(vmin=min_val.m, vmax=max_val.m)
451 # Create a ScalarMappable to use the colormap
452 sm = plt.cm.ScalarMappable(cmap=cmap, norm=normalize)
453 sm.set_array([])
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)
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
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
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.
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.
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)
493 # Get the color corresponding to the normalized value
494 color = cmap(normalized_value)
496 return to_hex(color, keep_alpha=False)
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.
508 """
509 save_path_demand = (save_path /
510 f"{data.lower()}.pdf")
511 y_values = df[data]
512 color = cm.RWTHBlau.p(100)
514 label_pad = 5
515 # Create a new figure with specified size
516 fig = plt.figure(figsize=fig_size, dpi=dpi)
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)
521 # Determine if y-axis needs to be in kilowatts
522 y_values = y_values.pint.to(ureg.degree_Celsius)
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
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='-')
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])
543 # Rotate the tick labels for better visibility
544 plt.gcf().autofmt_xdate(rotation=45)
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)
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)
563 # Show or save the plot
564 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, format='pdf')
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.
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 }
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}
612 # Rename the columns
613 df = df.rename(columns=columns_to_rename)
615 # Drop columns that were not renamed
616 df = df[columns_to_rename.values()]
618 save_path_demand = (
619 save_path / "surface_temperatures.svg") if save_path else \
620 None
621 label_pad = 5
623 # Create a new figure with specified size
624 fig = plt.figure(figsize=fig_size, dpi=dpi)
626 # Get a colormap with enough colors for all columns
628 # Iterate over each column in the DataFrame
629 for i, column in enumerate(df.columns):
630 y_values = df[column]
632 # Escape underscores in column names for LaTeX formatting
633 # safe_column_name = column.replace('_', r'\_')
635 # Plot the data
636 plt.plot(df.index, y_values, label=column,
637 linewidth=1,
638 linestyle='-')
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)
644 # Rotate the tick labels for better visibility
645 plt.gcf().autofmt_xdate(rotation=45)
647 # Limits
648 plt.xlim(df.index[0], df.index[-1])
649 plt.ylim(df.min().min() * 0.99, df.max().max() * 1.01)
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)
656 # Add grid
657 plt.grid(True, linestyle='--', alpha=0.6)
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)
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)
669 # Save the plot if a path is provided
670 PlotBEPSResults.save_or_show_plot(save_path_demand, dpi, format='svg')
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.
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}
713 # Rename the columns
714 df = df.rename(columns=columns_to_rename)
716 # Drop columns that were not renamed
717 df = df[columns_to_rename.values()]
719 save_path_demand = (
720 save_path / f"{file_name}.{file_type}") if save_path else \
721 None
722 label_pad = 5
724 # Create a new figure with specified size
725 fig = plt.figure(figsize=fig_size, dpi=dpi)
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]
732 # Escape underscores in column names for LaTeX formatting
733 # safe_column_name = column.replace('_', r'\_')
735 # Plot the data
736 plt.plot(df.index, y_values, label=column,
737 linewidth=1,
738 linestyle='-')
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)
744 # Rotate the tick labels for better visibility
745 plt.gcf().autofmt_xdate(rotation=45)
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)
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)
757 # Add grid
758 plt.grid(True, linestyle='--', alpha=0.6)
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)
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)
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
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)