Visualize reworked v2 (#267)

* initial commit

* Delete duplicate openapi.json

* revert temp cahnges

* test fixed

* mypy fixes

* mypy fixed

* Financial Overview included

* test data save path

* almost done

* mypy fix

* Update visualize.py

* ruff fix

* config, label

* improved start_hour vis

* fix

* fix2
This commit is contained in:
Normann 2024-12-24 13:10:31 +01:00 committed by GitHub
parent 5f898e8aab
commit bec24588e1
8 changed files with 552 additions and 574 deletions

View File

@ -21,7 +21,6 @@ from akkudoktoreos.devices.battery import (
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Inverter, InverterParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters
from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.utils.utils import NumpyEncoder
from akkudoktoreos.visualize import visualisiere_ergebnisse
class OptimizationParameters(BaseModel): class OptimizationParameters(BaseModel):
@ -520,19 +519,20 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin) ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
# Visualize the results # Visualize the results
visualisiere_ergebnisse( visualize = {
parameters.ems.gesamtlast, "ac_charge": ac_charge.tolist(),
parameters.ems.pv_prognose_wh, "dc_charge": dc_charge.tolist(),
parameters.ems.strompreis_euro_pro_wh, "discharge_allowed": discharge.tolist(),
o, "eautocharge_hours_float": eautocharge_hours_float,
ac_charge, "result": o,
dc_charge, "eauto_obj": self.ems.eauto.to_dict(),
discharge, "start_solution": start_solution,
parameters.temperature_forecast, "spuelstart": washingstart_int,
start_hour, "extra_data": extra_data,
einspeiseverguetung_euro_pro_wh, }
extra_data=extra_data, from akkudoktoreos.utils.visualize import prepare_visualize
)
prepare_visualize(parameters, visualize, start_hour=start_hour)
return OptimizeResponse( return OptimizeResponse(
**{ **{

View File

@ -0,0 +1,504 @@
import os
from collections.abc import Sequence
from typing import Callable, Optional, Union
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_pdf import PdfPages
from akkudoktoreos.core.coreabc import ConfigMixin
from akkudoktoreos.optimization.genetic import OptimizationParameters
class VisualizationReport(ConfigMixin):
def __init__(self, filename: str = "visualization_results.pdf") -> None:
# Initialize the report with a given filename and empty groups
self.filename = filename
self.groups: list[list[Callable[[], None]]] = [] # Store groups of charts
self.current_group: list[
Callable[[], None]
] = [] # Store current group of charts being created
self.pdf_pages = PdfPages(filename, metadata={}) # Initialize PdfPages without metadata
def add_chart_to_group(self, chart_func: Callable[[], None]) -> None:
"""Add a chart function to the current group."""
self.current_group.append(chart_func)
def finalize_group(self) -> None:
"""Finalize the current group and prepare for a new group."""
if self.current_group: # Check if current group has charts
self.groups.append(self.current_group) # Add current group to groups
else:
print("Finalizing an empty group!") # Warn if group is empty
self.current_group = [] # Reset current group for new charts
def _initialize_pdf(self) -> None:
"""Create the output directory if it doesn't exist and initialize the PDF."""
output_dir = self.config.data_output_path
# If self.filename is already a valid path, use it; otherwise, combine it with output_dir
if os.path.isabs(self.filename):
output_file = self.filename
else:
output_dir.mkdir(parents=True, exist_ok=True)
output_file = os.path.join(output_dir, self.filename)
self.pdf_pages = PdfPages(
output_file, metadata={}
) # Re-initialize PdfPages without metadata
def _save_group_to_pdf(self, group: list[Callable[[], None]]) -> None:
"""Save a group of charts to the PDF."""
fig_count = len(group) # Number of charts in the group
if fig_count == 0:
print("Attempted to save an empty group to PDF!") # Warn if group is empty
return # Prevent saving an empty group
# Create a figure layout based on the number of charts
if fig_count == 3:
# Layout for three charts: 1 full-width on top, 2 below
fig = plt.figure(figsize=(14, 10)) # Set a larger figure size
ax1 = fig.add_subplot(2, 1, 1) # Full-width subplot
ax2 = fig.add_subplot(2, 2, 3) # Bottom left subplot
ax3 = fig.add_subplot(2, 2, 4) # Bottom right subplot
# Store axes in a list for easy access
axs = [ax1, ax2, ax3]
else:
# Dynamic layout for any other number of charts
cols = 2 if fig_count > 1 else 1 # Determine number of columns
rows = (fig_count // 2) + (fig_count % 2) # Calculate required rows
fig, axs = plt.subplots(rows, cols, figsize=(14, 7 * rows)) # Create subplots
# If axs is a 2D array of axes, flatten it into a 1D list
# if isinstance(axs, np.ndarray):
axs = list(np.array(axs).reshape(-1))
# Draw each chart in the corresponding axes
for idx, chart_func in enumerate(group):
plt.sca(axs[idx]) # Set current axes
chart_func() # Call the chart function to draw
# Hide any unused axes
for idx in range(fig_count, len(axs)):
axs[idx].set_visible(False) # Hide unused axes
self.pdf_pages.savefig(fig) # Save the figure to the PDF
plt.close(fig) # Close the figure to free up memory
def create_line_chart(
self,
start_hour: Optional[int],
y_list: list[Union[np.ndarray, list[float]]],
title: str,
xlabel: str,
ylabel: str,
labels: Optional[list[str]] = None,
markers: Optional[list[str]] = None,
line_styles: Optional[list[str]] = None,
) -> None:
"""Create a line chart and add it to the current group."""
def chart() -> None:
nonlocal start_hour # Allow modifying `x` within the nested function
if start_hour is None:
start_hour = 0
first_element = y_list[0]
x: np.ndarray
# Case 1: y_list contains np.ndarray elements
if isinstance(first_element, np.ndarray):
x = np.arange(
start_hour, start_hour + len(first_element)
) # Start at x and extend by ndarray length
# Case 2: y_list contains float elements (1D list)
elif isinstance(first_element, float):
x = np.arange(
start_hour, start_hour + len(y_list)
) # Start at x and extend by list length
# Case 3: y_list is a nested list of floats
elif isinstance(first_element, list) and all(
isinstance(i, float) for i in first_element
):
max_len = max(len(sublist) for sublist in y_list)
x = np.arange(
start_hour, start_hour + max_len
) # Start at x and extend by max sublist length
else:
print(f"Unsupported y_list structure: {type(y_list)}, {y_list}")
raise TypeError(
"y_list elements must be np.ndarray, float, or a nested list of floats"
)
for idx, y_data in enumerate(y_list):
label = labels[idx] if labels else None # Chart label
marker = markers[idx] if markers and idx < len(markers) else "o" # Marker style
line_style = (
line_styles[idx] if line_styles and idx < len(line_styles) else "-"
) # Line style
plt.plot(x, y_data, label=label, marker=marker, linestyle=line_style) # Plot line
plt.title(title) # Set title
plt.xlabel(xlabel) # Set x-axis label
plt.ylabel(ylabel) # Set y-axis label
if labels:
plt.legend() # Show legend if labels are provided
plt.grid(True) # Show grid
plt.xlim(x[0] - 0.5, x[-1] + 0.5) # Adjust x-limits
self.add_chart_to_group(chart) # Add chart function to current group
def create_scatter_plot(
self,
x: np.ndarray,
y: np.ndarray,
title: str,
xlabel: str,
ylabel: str,
c: Optional[np.ndarray] = None,
) -> None:
"""Create a scatter plot and add it to the current group."""
def chart() -> None:
scatter = plt.scatter(x, y, c=c, cmap="viridis") # Create scatter plot
plt.title(title) # Set title
plt.xlabel(xlabel) # Set x-axis label
plt.ylabel(ylabel) # Set y-axis label
if c is not None:
plt.colorbar(scatter, label="Constraint") # Add colorbar if color data is provided
plt.grid(True) # Show grid
self.add_chart_to_group(chart) # Add chart function to current group
def create_bar_chart(
self,
labels: list[str],
values_list: Sequence[Union[int, float, list[Union[int, float]]]],
title: str,
ylabel: str,
xlabels: Optional[list[str]] = None,
label_names: Optional[list[str]] = None,
colors: Optional[list[str]] = None,
bar_width: float = 0.35,
bottom: Optional[int] = None,
) -> None:
"""Create a bar chart and add it to the current group."""
def chart() -> None:
num_groups = len(values_list) # Number of data groups
num_bars = len(labels) # Number of bars (categories)
# Calculate the positions for each bar group on the x-axis
x = np.arange(num_bars) # x positions for bars
offset = np.linspace(
-bar_width * (num_groups - 1) / 2, bar_width * (num_groups - 1) / 2, num_groups
) # Bar offsets
for i, values in enumerate(values_list):
bottom_use = None
if bottom == i + 1: # Set bottom if specified
bottom_use = 1
color = colors[i] if colors and i < len(colors) else None # Bar color
label_name = label_names[i] if label_names else None # Bar label
plt.bar(
x + offset[i],
values,
bar_width,
label=label_name,
color=color,
zorder=2,
alpha=0.6,
bottom=bottom_use,
) # Create bar
if xlabels:
plt.xticks(x, labels) # Add custom labels to the x-axis
plt.title(title) # Set title
plt.ylabel(ylabel) # Set y-axis label
if colors and label_names:
plt.legend() # Show legend if colors are provided
plt.grid(True, zorder=0) # Show grid in the background
plt.xlim(-0.5, len(labels) - 0.5) # Set x-axis limits
self.add_chart_to_group(chart) # Add chart function to current group
def create_violin_plot(
self, data_list: list[np.ndarray], labels: list[str], title: str, xlabel: str, ylabel: str
) -> None:
"""Create a violin plot and add it to the current group."""
def chart() -> None:
plt.violinplot(data_list, showmeans=True, showmedians=True) # Create violin plot
plt.xticks(np.arange(1, len(labels) + 1), labels) # Set x-ticks and labels
plt.title(title) # Set title
plt.xlabel(xlabel) # Set x-axis label
plt.ylabel(ylabel) # Set y-axis label
plt.grid(True) # Show grid
self.add_chart_to_group(chart) # Add chart function to current group
def generate_pdf(self) -> None:
"""Generate the PDF report with all the added chart groups."""
self._initialize_pdf() # Initialize the PDF
for group in self.groups:
self._save_group_to_pdf(group) # Save each group to the PDF
self.pdf_pages.close() # Close the PDF to finalize the report
def prepare_visualize(
parameters: OptimizationParameters,
results: dict,
filename: str = "visualization_results_new.pdf",
start_hour: Optional[int] = 0,
) -> None:
report = VisualizationReport(filename)
# Group 1:
report.create_line_chart(
None,
[parameters.ems.gesamtlast],
title="Load Profile",
xlabel="Hours",
ylabel="Load (Wh)",
labels=["Total Load (Wh)"],
markers=["s"],
line_styles=["-"],
)
report.create_line_chart(
None,
[parameters.ems.pv_prognose_wh],
title="PV Forecast",
xlabel="Hours",
ylabel="PV Generation (Wh)",
)
report.create_line_chart(
None,
[np.full(len(parameters.ems.gesamtlast), parameters.ems.einspeiseverguetung_euro_pro_wh)],
title="Remuneration",
xlabel="Hours",
ylabel="€/Wh",
)
if parameters.temperature_forecast:
report.create_line_chart(
None,
[parameters.temperature_forecast],
title="Temperature Forecast",
xlabel="Hours",
ylabel="°C",
)
report.finalize_group()
# Group 2:
report.create_line_chart(
start_hour,
[
results["result"]["Last_Wh_pro_Stunde"],
results["result"]["Home_appliance_wh_per_hour"],
results["result"]["Netzeinspeisung_Wh_pro_Stunde"],
results["result"]["Netzbezug_Wh_pro_Stunde"],
results["result"]["Verluste_Pro_Stunde"],
],
title="Energy Flow per Hour",
xlabel="Hours",
ylabel="Energy (Wh)",
labels=[
"Load (Wh)",
"Household Device (Wh)",
"Grid Feed-in (Wh)",
"Grid Consumption (Wh)",
"Losses (Wh)",
],
markers=["o", "o", "x", "^", "^"],
line_styles=["-", "--", ":", "-.", "-"],
)
report.finalize_group()
# Group 3:
report.create_line_chart(
start_hour,
[results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]],
title="Battery SOC",
xlabel="Hours",
ylabel="%",
labels=[
"Battery SOC (%)",
"Electric Vehicle SOC (%)",
],
markers=["o", "x"],
)
report.create_line_chart(
None,
[parameters.ems.strompreis_euro_pro_wh],
title="Electricity Price",
xlabel="Hours",
ylabel="Price (€/Wh)",
)
report.create_bar_chart(
list(str(i) for i in range(len(results["ac_charge"]))),
[results["ac_charge"], results["dc_charge"], results["discharge_allowed"]],
title="AC/DC Charging and Discharge Overview",
ylabel="Relative Power (0-1) / Discharge (0 or 1)",
label_names=["AC Charging (relative)", "DC Charging (relative)", "Discharge Allowed"],
colors=["blue", "green", "red"],
bottom=3,
)
report.finalize_group()
# Group 4:
report.create_line_chart(
start_hour,
[
results["result"]["Kosten_Euro_pro_Stunde"],
results["result"]["Einnahmen_Euro_pro_Stunde"],
],
title="Financial Balance per Hour",
xlabel="Hours",
ylabel="Euro",
labels=["Costs", "Revenue"],
)
extra_data = results["extra_data"]
report.create_scatter_plot(
extra_data["verluste"],
extra_data["bilanz"],
title="",
xlabel="losses",
ylabel="balance",
c=extra_data["nebenbedingung"],
)
# Example usage
values_list = [
[
results["result"]["Gesamtkosten_Euro"],
results["result"]["Gesamteinnahmen_Euro"],
results["result"]["Gesamtbilanz_Euro"],
]
]
labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"]
report.create_bar_chart(
labels=labels,
values_list=values_list,
title="Financial Overview",
ylabel="Euro",
xlabels=["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"],
)
report.finalize_group()
# Group 1: Scatter plot of losses vs balance with color-coded constraints
f1 = np.array(extra_data["verluste"]) # Losses
f2 = np.array(extra_data["bilanz"]) # Balance
n1 = np.array(extra_data["nebenbedingung"]) # Constraints
# Filter data where 'nebenbedingung' < 0.01
filtered_indices = n1 < 0.01
filtered_losses = f1[filtered_indices]
filtered_balance = f2[filtered_indices]
# Group 2: Violin plot for filtered losses
if filtered_losses.size > 0:
report.create_violin_plot(
data_list=[filtered_losses], # Data for filtered losses
labels=["Filtered Losses"], # Label for the violin plot
title="Violin Plot for Filtered Losses (Constraint < 0.01)",
xlabel="Losses",
ylabel="Values",
)
else:
print("No data available for filtered losses violin plot (Constraint < 0.01)")
# Group 3: Violin plot for filtered balance
if filtered_balance.size > 0:
report.create_violin_plot(
data_list=[filtered_balance], # Data for filtered balance
labels=["Filtered Balance"], # Label for the violin plot
title="Violin Plot for Filtered Balance (Constraint < 0.01)",
xlabel="Balance",
ylabel="Values",
)
else:
print("No data available for filtered balance violin plot (Constraint < 0.01)")
if filtered_balance.size > 0 or filtered_losses.size > 0:
report.finalize_group()
# Generate the PDF report
report.generate_pdf()
if __name__ == "__main__":
# Example usage
report = VisualizationReport("example_report.pdf")
x_hours = 0 # Define x-axis start values (e.g., hours)
# Group 1: Adding charts to be displayed on the same page
report.create_line_chart(
x_hours,
[np.array([10, 20, 30, 40])],
title="Load Profile",
xlabel="Hours",
ylabel="Load (Wh)",
)
report.create_line_chart(
x_hours,
[np.array([5, 15, 25, 35])],
title="PV Forecast",
xlabel="Hours",
ylabel="PV Generation (Wh)",
)
report.create_line_chart(
x_hours,
[np.array([5, 15, 25, 35])],
title="PV Forecast",
xlabel="Hours",
ylabel="PV Generation (Wh)",
)
# Note: If there are only 3 charts per page, the first is as wide as the page
report.finalize_group() # Finalize the first group of charts
# Group 2: Adding more charts to be displayed on another page
report.create_line_chart(
x_hours,
[np.array([0.2, 0.25, 0.3, 0.35])],
title="Electricity Price",
xlabel="Hours",
ylabel="Price (€/Wh)",
)
report.create_bar_chart(
["Costs", "Revenue", "Balance"],
[[500.0], [600.0], [100.0]],
title="Financial Overview",
ylabel="Euro",
label_names=["AC Charging (relative)", "DC Charging (relative)", "Discharge Allowed"],
colors=["red", "green", "blue"],
)
report.create_scatter_plot(
np.array([5, 6, 7, 8]),
np.array([100, 200, 150, 250]),
title="Scatter Plot",
xlabel="Losses",
ylabel="Balance",
c=np.array([0.1, 0.2, 0.3, 0.4]),
)
report.finalize_group() # Finalize the second group of charts
# Group 3: Adding a violin plot
data = [np.random.normal(0, std, 100) for std in range(1, 5)] # Example data for violin plot
report.create_violin_plot(
data,
labels=["Group 1", "Group 2", "Group 3", "Group 4"],
title="Violin Plot",
xlabel="Groups",
ylabel="Values",
)
data = [np.random.normal(0, 1, 100)] # Example data for violin plot
report.create_violin_plot(
data, labels=["Group 1"], title="Violin Plot", xlabel="Group", ylabel="Values"
)
report.finalize_group() # Finalize the third group of charts
# Generate the PDF report
report.generate_pdf()

View File

@ -1,350 +0,0 @@
# Set the backend for matplotlib to Agg
from typing import Any, Optional
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_pdf import PdfPages
from numpy.typing import NDArray
from akkudoktoreos.config.config import get_config
matplotlib.use("Agg")
def visualisiere_ergebnisse(
gesamtlast: list[float],
pv_forecast: list[float],
strompreise: list[float],
ergebnisse: dict[str, Any],
ac: np.ndarray, # AC charging allowed
dc: np.ndarray, # DC charging allowed
discharge: np.ndarray, # Discharge allowed
temperature: Optional[list[float]],
start_hour: int,
einspeiseverguetung_euro_pro_wh: np.ndarray,
filename: str = "visualization_results.pdf",
extra_data: Optional[dict[str, Any]] = None,
) -> None:
#####################
# 24-hour visualization
#####################
config = get_config()
output_dir = config.data_output_path
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir.joinpath(filename)
with PdfPages(output_file) as pdf:
# Load and PV generation
plt.figure(figsize=(14, 14))
plt.subplot(3, 3, 1)
hours: NDArray[np.int_] = np.arange(0, config.prediction_hours, dtype=np.int_)
gesamtlast_array = np.array(gesamtlast)
# Plot individual loads
plt.plot(hours, gesamtlast_array, label="Load (Wh)", marker="o")
# Calculate and plot total load
plt.plot(
hours,
gesamtlast_array,
label="Total Load (Wh)",
marker="o",
linewidth=2,
linestyle="--",
)
plt.xlabel("Hour")
plt.ylabel("Load (Wh)")
plt.title("Load Profiles")
plt.grid(True)
plt.legend()
# PV forecast
plt.subplot(3, 2, 3)
plt.plot(hours, pv_forecast, label="PV Generation (Wh)", marker="x")
plt.title("PV Forecast")
plt.xlabel("Hour of the Day")
plt.ylabel("Wh")
plt.legend()
plt.grid(True)
# Feed-in remuneration
plt.subplot(3, 2, 4)
plt.plot(
hours,
einspeiseverguetung_euro_pro_wh,
label="Remuneration (€/Wh)",
marker="x",
)
plt.title("Remuneration")
plt.xlabel("Hour of the Day")
plt.ylabel("€/Wh")
plt.legend()
plt.grid(True)
# Temperature forecast
if temperature is not None:
plt.subplot(3, 2, 5)
plt.title("Temperature Forecast (°C)")
plt.plot(hours, temperature, label="Temperature (°C)", marker="x")
plt.xlabel("Hour of the Day")
plt.ylabel("°C")
plt.legend()
plt.grid(True)
pdf.savefig() # Save the current figure state to the PDF
plt.close() # Close the current figure to free up memory
#####################
# Start hour visualization
#####################
plt.figure(figsize=(14, 10))
hours = np.arange(start_hour, config.prediction_hours)
# Energy flow, grid feed-in, and grid consumption
plt.subplot(3, 2, 1)
# Plot with transparency (alpha) and different linestyles
plt.plot(
hours,
ergebnisse["Last_Wh_pro_Stunde"],
label="Load (Wh)",
marker="o",
linestyle="-",
alpha=0.8,
)
plt.plot(
hours,
ergebnisse["Home_appliance_wh_per_hour"],
label="Household Device (Wh)",
marker="o",
linestyle="--",
alpha=0.8,
)
plt.plot(
hours,
ergebnisse["Netzeinspeisung_Wh_pro_Stunde"],
label="Grid Feed-in (Wh)",
marker="x",
linestyle=":",
alpha=0.8,
)
plt.plot(
hours,
ergebnisse["Netzbezug_Wh_pro_Stunde"],
label="Grid Consumption (Wh)",
marker="^",
linestyle="-.",
alpha=0.8,
)
plt.plot(
hours,
ergebnisse["Verluste_Pro_Stunde"],
label="Losses (Wh)",
marker="^",
linestyle="-",
alpha=0.8,
)
# Title and labels
plt.title("Energy Flow per Hour")
plt.xlabel("Hour")
plt.ylabel("Energy (Wh)")
# Show legend with a higher number of columns to avoid overlap
plt.legend(ncol=2)
# Electricity prices
hours_p = np.arange(0, len(strompreise))
plt.subplot(3, 2, 3)
plt.plot(
hours_p,
strompreise,
label="Electricity Price (€/Wh)",
color="purple",
marker="s",
)
plt.title("Electricity Prices")
plt.xlabel("Hour of the Day")
plt.ylabel("Price (€/Wh)")
plt.legend()
plt.grid(True)
# State of charge for batteries
plt.subplot(3, 2, 2)
plt.plot(hours, ergebnisse["akku_soc_pro_stunde"], label="PV Battery (%)", marker="x")
plt.plot(
hours,
ergebnisse["EAuto_SoC_pro_Stunde"],
label="E-Car Battery (%)",
marker="x",
)
plt.legend(loc="upper left", bbox_to_anchor=(1, 1)) # Place legend outside the plot
plt.grid(True, which="both", axis="x") # Grid for every hour
# Plot for AC, DC charging, and Discharge status using bar charts
ax1 = plt.subplot(3, 2, 5)
hours = np.arange(0, config.prediction_hours)
# Plot AC charging as bars (relative values between 0 and 1)
plt.bar(hours, ac, width=0.4, label="AC Charging (relative)", color="blue", alpha=0.6)
# Plot DC charging as bars (relative values between 0 and 1)
plt.bar(
hours + 0.4, dc, width=0.4, label="DC Charging (relative)", color="green", alpha=0.6
)
# Plot Discharge as bars (0 or 1, binary values)
plt.bar(
hours,
discharge,
width=0.4,
label="Discharge Allowed",
color="red",
alpha=0.6,
bottom=np.maximum(ac, dc),
)
# Configure the plot
ax1.legend(loc="upper left")
ax1.set_xlim(0, config.prediction_hours)
ax1.set_xlabel("Hour")
ax1.set_ylabel("Relative Power (0-1) / Discharge (0 or 1)")
ax1.set_title("AC/DC Charging and Discharge Overview")
ax1.grid(True)
hours = np.arange(start_hour, config.prediction_hours)
pdf.savefig() # Save the current figure state to the PDF
plt.close() # Close the current figure to free up memory
# Financial overview
fig, axs = plt.subplots(1, 2, figsize=(14, 10)) # Create a 1x2 grid of subplots
total_costs = ergebnisse["Gesamtkosten_Euro"]
total_revenue = ergebnisse["Gesamteinnahmen_Euro"]
total_balance = ergebnisse["Gesamtbilanz_Euro"]
losses = ergebnisse["Gesamt_Verluste"]
# Costs and revenues per hour on the first axis (axs[0])
costs = ergebnisse["Kosten_Euro_pro_Stunde"]
revenues = ergebnisse["Einnahmen_Euro_pro_Stunde"]
# Plot costs
axs[0].plot(
hours,
costs,
label="Costs (Euro)",
marker="o",
color="red",
)
# Annotate costs
for hour, value in enumerate(costs):
if value is None or np.isnan(value):
value = 0
axs[0].annotate(
f"{value:.2f}",
(hour, value),
textcoords="offset points",
xytext=(0, 5),
ha="center",
fontsize=8,
color="red",
)
# Plot revenues
axs[0].plot(
hours,
revenues,
label="Revenue (Euro)",
marker="x",
color="green",
)
# Annotate revenues
for hour, value in enumerate(revenues):
if value is None or np.isnan(value):
value = 0
axs[0].annotate(
f"{value:.2f}",
(hour, value),
textcoords="offset points",
xytext=(0, 5),
ha="center",
fontsize=8,
color="green",
)
# Title and labels
axs[0].set_title("Financial Balance per Hour")
axs[0].set_xlabel("Hour")
axs[0].set_ylabel("Euro")
axs[0].legend()
axs[0].grid(True)
# Summary of finances on the second axis (axs[1])
labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"]
values = [total_costs, total_revenue, total_balance]
colors = ["red" if value > 0 else "green" for value in values]
axs[1].bar(labels, values, color=colors)
axs[1].set_title("Financial Overview")
axs[1].set_ylabel("Euro")
# Second axis (ax2) for losses, shared with axs[1]
ax2 = axs[1].twinx()
ax2.bar("Total Losses", losses, color="blue")
ax2.set_ylabel("Losses [Wh]", color="blue")
ax2.tick_params(axis="y", labelcolor="blue")
pdf.savefig() # Save the complete figure to the PDF
plt.close() # Close the figure
# Additional data visualization if provided
if extra_data is not None:
plt.figure(figsize=(14, 10))
plt.subplot(1, 2, 1)
f1 = np.array(extra_data["verluste"])
f2 = np.array(extra_data["bilanz"])
n1 = np.array(extra_data["nebenbedingung"])
scatter = plt.scatter(f1, f2, c=n1, cmap="viridis")
# Add color legend
plt.colorbar(scatter, label="Constraint")
pdf.savefig() # Save the complete figure to the PDF
plt.close() # Close the figure
plt.figure(figsize=(14, 10))
filtered_losses = np.array(
[
v
for v, n in zip(extra_data["verluste"], extra_data["nebenbedingung"])
if n < 0.01
]
)
filtered_balance = np.array(
[b for b, n in zip(extra_data["bilanz"], extra_data["nebenbedingung"]) if n < 0.01]
)
if filtered_losses.size != 0:
best_loss = min(filtered_losses)
worst_loss = max(filtered_losses)
best_balance = min(filtered_balance)
worst_balance = max(filtered_balance)
data = [filtered_losses, filtered_balance]
labels = ["Losses", "Balance"]
# Create plots
fig, axs = plt.subplots(
1, 2, figsize=(10, 6), sharey=False
) # Two subplots, separate y-axes
# First violin plot for losses
axs[0].violinplot(data[0], positions=[1], showmeans=True, showmedians=True)
axs[0].set(xticks=[1], xticklabels=["Losses"])
# Second violin plot for balance
axs[1].violinplot(data[1], positions=[1], showmeans=True, showmedians=True)
axs[1].set(xticks=[1], xticklabels=["Balance"])
# Fine-tuning
plt.tight_layout()
pdf.savefig() # Save the current figure state to the PDF
plt.close() # Close the figure

View File

@ -11,6 +11,9 @@ from akkudoktoreos.optimization.genetic import (
OptimizeResponse, OptimizeResponse,
optimization_problem, optimization_problem,
) )
from akkudoktoreos.utils.visualize import (
prepare_visualize, # Import the new prepare_visualize
)
DIR_TESTDATA = Path(__file__).parent / "testdata" DIR_TESTDATA = Path(__file__).parent / "testdata"
@ -64,16 +67,12 @@ def test_optimize(fn_in: str, fn_out: str, ngen: int, is_full_run: bool):
visualize_filename = str((DIR_TESTDATA / f"new_{fn_out}").with_suffix(".pdf")) visualize_filename = str((DIR_TESTDATA / f"new_{fn_out}").with_suffix(".pdf"))
def visualize_to_file(*args, **kwargs):
from akkudoktoreos.visualize import visualisiere_ergebnisse
# Write test output pdf to file, so we can look at it manually
kwargs["filename"] = visualize_filename
return visualisiere_ergebnisse(*args, **kwargs)
with patch( with patch(
"akkudoktoreos.optimization.genetic.visualisiere_ergebnisse", side_effect=visualize_to_file "akkudoktoreos.utils.visualize.prepare_visualize",
) as visualisiere_ergebnisse_patch: side_effect=lambda parameters, results, *args, **kwargs: prepare_visualize(
parameters, results, filename=visualize_filename, **kwargs
),
) as prepare_visualize_patch:
# Call the optimization function # Call the optimization function
ergebnis = opt_class.optimierung_ems( ergebnis = opt_class.optimierung_ems(
parameters=input_data, start_hour=start_hour, ngen=ngen parameters=input_data, start_hour=start_hour, ngen=ngen
@ -92,4 +91,4 @@ def test_optimize(fn_in: str, fn_out: str, ngen: int, is_full_run: bool):
compare_dict(ergebnis.model_dump(), expected_result.model_dump()) compare_dict(ergebnis.model_dump(), expected_result.model_dump())
# The function creates a visualization result PDF as a side-effect. # The function creates a visualization result PDF as a side-effect.
visualisiere_ergebnisse_patch.assert_called_once() prepare_visualize_patch.assert_called_once()

View File

@ -1,36 +1,36 @@
import json import os
import subprocess
from pathlib import Path from pathlib import Path
import pytest
from matplotlib.testing.compare import compare_images from matplotlib.testing.compare import compare_images
from akkudoktoreos.config.config import get_config from akkudoktoreos.config.config import get_config
from akkudoktoreos.visualize import visualisiere_ergebnisse
filename = "example_report.pdf"
config = get_config()
output_dir = config.data_output_path
output_dir.mkdir(parents=True, exist_ok=True)
output_file = os.path.join(output_dir, filename)
DIR_TESTDATA = Path(__file__).parent / "testdata" DIR_TESTDATA = Path(__file__).parent / "testdata"
DIR_IMAGEDATA = DIR_TESTDATA / "images" reference_file = DIR_TESTDATA / "test_example_report.pdf"
@pytest.mark.parametrize( def test_generate_pdf_main():
"fn_in, fn_out, fn_out_base", # Delete the old generated file if it exists
[("visualize_input_1.json", "visualize_output_1.pdf", "visualize_base_output_1.pdf")], if os.path.isfile(output_file):
) os.remove(output_file)
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base):
with open(DIR_TESTDATA / fn_in, "r") as f:
input_data = json.load(f)
visualisiere_ergebnisse(**input_data)
config = get_config() # Execute the __main__ block of visualize.py by running it as a script
output_dir = config.data_output_path script_path = Path(__file__).parent.parent / "src" / "akkudoktoreos" / "utils" / "visualize.py"
output_dir.mkdir(parents=True, exist_ok=True) subprocess.run(["python", str(script_path)], check=True)
output_file = output_dir.joinpath(fn_out)
assert output_file.is_file() # Check if the file exists
assert ( assert os.path.isfile(output_file)
compare_images(
str(output_file), # Compare the generated file with the reference file
str(DIR_IMAGEDATA / fn_out_base), comparison = compare_images(str(reference_file), str(output_file), tol=0)
0,
) # Assert that there are no differences
is None assert comparison is None, f"Images differ: {comparison}"
)

Binary file not shown.

BIN
tests/testdata/test_example_report.pdf vendored Normal file

Binary file not shown.

View File

@ -1,175 +0,0 @@
{
"gesamtlast": [
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68,
995.0, 1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22,
1103.78, 1129.12, 1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05,
694.34, 608.79, 556.31, 488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46,
1155.99, 827.01, 1257.98, 1232.67, 871.26, 860.88, 1158.03, 1222.72,
1221.04, 949.99, 987.01, 733.99, 592.97
],
"pv_forecast": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5000.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0
],
"strompreise": [
0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001,
0.00001, 0.00001, 0.001, 0.00005, 0.00005, 0.00005, 0.00005, 0.001, 0.001,
0.001, 0.001, 0.001, 0.00001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001,
0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001,
0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001
],
"ergebnisse": {
"Last_Wh_pro_Stunde": [
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15,
3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 1320.56, 1132.03,
1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 988.56,
2035.7436363636361, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01,
1257.98, 1232.67, 871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99,
987.01, 733.99, 592.97
],
"Netzeinspeisung_Wh_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3679.44, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0
],
"Netzbezug_Wh_pro_Stunde": [
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15,
3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 0.0, 1132.03,
1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 0.0,
2035.7436363636361, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01,
1257.98, 1232.67, 871.26, 860.88, 1158.03, 0.0, 1221.04, 0.0, 0.0, 0.0,
592.97
],
"Kosten_Euro_pro_Stunde": [
0.1249371, 0.10502190000000002, 0.1277513, 0.1535688, 0.1146838,
0.0403795, 0.060471500000000004, 0.0311228, 0.0321168, 0.00995, 1.05307,
0.05319550000000001, 0.0, 0.0566015, 0.058183500000000006, 1.17682,
1.21622, 1.10378, 1.12912, 1.1787100000000001, 0.010509800000000001, 0.0,
2.035743636363636, 0.7046100000000001, 0.51637, 0.86805,
0.6943400000000001, 0.6087899999999999, 0.55631, 0.48889,
0.5069100000000001, 0.80489, 1.14198, 1.05697, 0.99246, 1.15599, 0.82701,
1.25798, 1.2326700000000002, 0.87126, 0.86088, 1.15803, 0.0, 1.22104, 0.0,
0.0, 0.0, 0.59297
],
"akku_soc_pro_stunde": [
25.0, 31.666666666666664, 38.333333333333336, 55.00000000000001,
61.66666666666667, 75.0, 81.66666666666667, 91.66666666666666, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 95.7448347107438, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 94.73691460055097, 94.73691460055097,
90.64777031680441, 86.39927685950414, 83.23988464187329, 83.23988464187329
],
"Einnahmen_Euro_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2575608,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 27.732796636363638,
"EAuto_SoC_pro_Stunde": [
30.294999999999998, 43.405, 60.885, 78.365, 93.66, 93.66, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0
],
"Gesamteinnahmen_Euro": 0.2575608,
"Gesamtkosten_Euro": 27.990357436363638,
"Verluste_Pro_Stunde": [
843.0, 654.0, 792.0, 1152.0, 723.0, 480.0, 440.2105263157896, 360.0,
300.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
134.80363636363631, 153.18595041322305, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
166.73454545454547, 0.0, 129.54409090909098, 134.59227272727276,
100.08954545454549, 0.0
],
"Gesamt_Verluste": 6563.160567638104,
"Home_appliance_wh_per_hour": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
"ac": [
0.6, 0.4, 0.4, 1.0, 0.4, 0.8, 0.4, 0.6, 1.0, 0.2, 0.2, 0.2, 0.6, 0.0, 0.2,
0.6, 0.0, 0.0, 0.8, 0.8, 0.4, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.2, 0.8,
0.2, 0.4, 0.6, 1.0, 0.0, 1.0, 0.8, 0.4, 0.4, 1.0, 0.2, 0.6, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0
],
"dc": [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
],
"discharge": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0
],
"temperature": [
18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7,
17.4, 18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0,
12.6, 12.2, 11.7, 11.6, 11.3, 11.0, 10.7, 10.2, 11.4, 14.4, 16.4, 18.3,
19.5, 20.7, 21.9, 22.7, 23.1, 23.1, 22.8, 21.8, 20.2, 19.1, 18.0, 17.4
],
"start_hour": 0,
"einspeiseverguetung_euro_pro_wh": [
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007
],
"filename": "visualize_output_1.pdf",
"extra_data": null
}