mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-15 10:31:57 +00:00
chore: adapt pdf visualization (#1205)
Change PDF visualization to be created on demand and per optimization algorithm. The PDF for the GENETIC0 optimization is provided by the /visualization_results.pdf endpoint. There is no change in the interface. By this the optimization algorithm is offloaded from the PDF generation which spares some time. To cope with several users may call the /visualization_results.pdf endpoint at the same time the PDF is generated on the fly without any intermediate file taking the stored GENETIC0 solution as an input. SVG picture generation is removed as this would again create intermediate files. Chart pictures can easily be taken from the PDF. To allow on demand creation of the optimization results visualization the optimisation solution stored is extended by several new attributes. To keep the deprecated /optimize endpoint compatible the optimization solution is stripped to the legacy content before returned. Due to the extension of the solution the optimization tests were adapted to cover the extended content. The optimization tests are adapted to test the generated visualization report by the pypdf reader. Pypdf is added to the development dependencies. Besides the adaptation several fixes and improvements are added: * feat: extend /v1/prediction/series endpoint by resampling and filling Add parameters for resampling and filling. Add the processing parameter to control wether raw data or resampled data shall be returned. * feat: extend /v1/measurement/series endpoint by resampling and filling Add parameters for resampling and filling: Add the processing parameter to control wether raw data or resampled data shall be returned. * feat: standardize and improve API error response Use FASTApi exception handlers to provide a standardized API exception handling. All exceptions are logged. Exception traces are only returned if the new logging configuration parameter logging.api_logging_level is set to "DEBUG" or "TRACE". Avoids unwanted leackage of server internals on exceptions. * fix: align to intervall when resampling Ensure resampling is aligned to interval also when the buckets are shifted due to the align_to_intervall parameter is set. * chore: make dropna mandatory and default to True * chore: refactor key_to_xxx data management methods Make key_to_series the central method for data resampling and fill. Add a new key_to_raw_series to retrieve the data as it is stored (without resampling and filling). Users of key_to_series were mostly moved to key_to_raw_series as this resembles the former interface. Especially in predictions and tests this was done. * chore: create test data sub-directory for each optimization algorithm To prevent cluttering the test data directory and ease test data management for optimization algorithms each algorithm got it's own sub-directory. The current test data was moved to these sub-directories. * chore: update version Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -1221,39 +1221,20 @@ class GeneticOptimization(OptimizationBase):
|
||||
else:
|
||||
discharge = discharge.tolist()
|
||||
|
||||
# Visualize the results in PDF
|
||||
try:
|
||||
from akkudoktoreos.utils.visualize import prepare_visualize
|
||||
|
||||
visualize = {
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"ev_charge_hours_float": ev_charge_hours_float,
|
||||
"result": GeneticSimulationResult(**simulation_result).model_dump(),
|
||||
"ev_obj": self.simulation.ev.to_dict() if self.simulation.ev else None,
|
||||
"start_solution": start_solution,
|
||||
"washingstart": washingstart_int,
|
||||
"extra_data": extra_data,
|
||||
"fitness_history": self.fitness_history,
|
||||
"fixed_seed": self.fix_seed,
|
||||
}
|
||||
|
||||
prepare_visualize(parameters, visualize, start_hour=start_hour)
|
||||
|
||||
except Exception as ex:
|
||||
error_msg = f"Visualization failed: {ex}"
|
||||
logger.error(error_msg)
|
||||
|
||||
return GeneticSolution(
|
||||
**{
|
||||
"parameters": parameters,
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"ev_charge_hours_float": ev_charge_hours_float,
|
||||
"result": GeneticSimulationResult(**simulation_result),
|
||||
"ev_obj": self.simulation.ev,
|
||||
"start_hour": start_hour,
|
||||
"start_solution": start_solution,
|
||||
"washingstart": washingstart_int,
|
||||
"extra_data": extra_data,
|
||||
"fitness_history": self.fitness_history,
|
||||
"fixed_seed": self.fix_seed,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Genetic algorithm optimisation solution."""
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -27,6 +27,9 @@ from akkudoktoreos.devices.devicesabc import (
|
||||
)
|
||||
from akkudoktoreos.devices.genetic.battery import Battery
|
||||
from akkudoktoreos.optimization.genetic.geneticdevices import GeneticParametersBaseModel
|
||||
from akkudoktoreos.optimization.genetic.geneticparams import (
|
||||
GeneticOptimizationParameters,
|
||||
)
|
||||
from akkudoktoreos.optimization.optimization import OptimizationSolution
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
|
||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
@@ -247,6 +250,9 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
||||
|
||||
parameters: GeneticOptimizationParameters = Field(
|
||||
json_schema_extra={"description": "Optimization parameters used to generate solution."}
|
||||
)
|
||||
ac_charge: list[float] = Field(
|
||||
json_schema_extra={
|
||||
"description": "Array with AC charging values as relative power (0.0-1.0), other values set to 0."
|
||||
@@ -273,6 +279,10 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
validation_alias=AliasChoices("ev_obj", "eauto_obj"),
|
||||
json_schema_extra={"description": "Electric vehicle state after optimization."},
|
||||
)
|
||||
start_hour: int = Field(
|
||||
default=0,
|
||||
json_schema_extra={"description": "Start hour."},
|
||||
)
|
||||
start_solution: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
@@ -285,6 +295,28 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
|
||||
"description": "Can be `null` or contain an object representing the start of washing (if applicable)."
|
||||
},
|
||||
)
|
||||
extra_data: Optional[dict[str, Union[list[int], list[float]]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": ("Dictionary of balance: TBD, losses: TBD, constraints: TBD.")
|
||||
},
|
||||
)
|
||||
fitness_history: Optional[dict[str, Union[list[int], list[float]]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Dictionary of "
|
||||
"gen: Generation numbers (X-axis), "
|
||||
"avg: Average fitness for each generation (Y-axis), "
|
||||
"max: Maximum fitness for each generation (Y-axis), "
|
||||
"min: Minimum fitness for each generation (Y-axis)."
|
||||
)
|
||||
},
|
||||
)
|
||||
fixed_seed: Optional[int] = Field(
|
||||
default=None,
|
||||
json_schema_extra={"description": "Fixed seed."},
|
||||
)
|
||||
|
||||
# Computed fields for backward compatibility (deprecated German names)
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
|
||||
@@ -0,0 +1,801 @@
|
||||
import json
|
||||
import textwrap
|
||||
from collections.abc import Sequence
|
||||
from io import BytesIO
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pendulum
|
||||
from matplotlib.backends.backend_pdf import PdfPages
|
||||
|
||||
from akkudoktoreos.core.coreabc import ConfigMixin, get_ems
|
||||
from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSolution
|
||||
from akkudoktoreos.utils.datetimeutil import DateTime, to_datetime
|
||||
|
||||
matplotlib.use(
|
||||
"Agg"
|
||||
) # non-interactive backend that can only write to files, backend needed to stay in main thread.
|
||||
|
||||
|
||||
debug_visualize: bool = False
|
||||
|
||||
|
||||
class GeneticVisualizationReport(ConfigMixin):
|
||||
def __init__(
|
||||
self,
|
||||
version: str = "0.0.1",
|
||||
) -> None:
|
||||
# Initialize the report with empty groups
|
||||
self.groups: list[list[Callable[[], None]]] = [] # Store groups of charts
|
||||
self.current_group: list[
|
||||
Callable[[], None]
|
||||
] = [] # Store current group of charts being created
|
||||
|
||||
# file like buffer to create the PDF
|
||||
self._buffer = BytesIO()
|
||||
self.pdf_pages: PdfPages | None = None
|
||||
|
||||
self.version = version # overwrite version as test for constant output of pdf for test
|
||||
self.current_time = to_datetime(
|
||||
as_string="YYYY-MM-DD HH:mm:ss", in_timezone=self.config.general.timezone
|
||||
)
|
||||
|
||||
def add_chart_to_group(self, chart_func: Callable[[], None], title: str | None) -> None:
|
||||
"""Add a chart function to the current group and save it as a PNG and SVG."""
|
||||
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:
|
||||
"""Initialize the PDF."""
|
||||
self._buffer = BytesIO()
|
||||
self.pdf_pages = PdfPages(self._buffer, metadata={}) # Initialize PdfPages without metadata
|
||||
|
||||
def _save_group_to_pdf(self, group: list[Callable[[], None]]) -> None:
|
||||
"""Save a group of charts to the PDF."""
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
fig_count = len(group) # Number of charts in the group
|
||||
|
||||
if fig_count == 0:
|
||||
print("Attempted to save an empty group to PDF!")
|
||||
return
|
||||
|
||||
# Check for special charts before creating layout
|
||||
special_keywords = {"add_text_page", "add_json_page"}
|
||||
for chart_func in group:
|
||||
if any(keyword in chart_func.__qualname__ for keyword in special_keywords):
|
||||
chart_func() # Special chart functions handle their own rendering
|
||||
return
|
||||
|
||||
# Create layout only if no special charts are detected
|
||||
if fig_count == 3:
|
||||
fig = plt.figure(figsize=(14, 10))
|
||||
ax1 = fig.add_subplot(2, 1, 1)
|
||||
ax2 = fig.add_subplot(2, 2, 3)
|
||||
ax3 = fig.add_subplot(2, 2, 4)
|
||||
axs = [ax1, ax2, ax3]
|
||||
else:
|
||||
cols = 2 if fig_count > 1 else 1
|
||||
rows = (fig_count + 1) // 2
|
||||
fig, axs = plt.subplots(rows, cols, figsize=(14, 7 * rows))
|
||||
axs = list(np.array(axs).reshape(-1))
|
||||
|
||||
# Add footer text with current time to each page
|
||||
if self.version == "test":
|
||||
current_time = "test"
|
||||
else:
|
||||
current_time = self.current_time
|
||||
fig.text(
|
||||
0.5,
|
||||
0.02,
|
||||
f"Generated on: {current_time} with version: {self.version}",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=10,
|
||||
)
|
||||
|
||||
# Render each chart in its corresponding axis
|
||||
for idx, chart_func in enumerate(group):
|
||||
plt.sca(axs[idx]) # Set current axis
|
||||
chart_func() # Render the chart
|
||||
|
||||
# Save the figure to the PDF and clean up
|
||||
for idx in range(fig_count, len(axs)):
|
||||
axs[idx].set_visible(False)
|
||||
|
||||
self.pdf_pages.savefig(fig) # Save the figure to the PDF
|
||||
plt.close(fig)
|
||||
|
||||
def create_line_chart_date(
|
||||
self,
|
||||
start_date: DateTime,
|
||||
y_list: list[Union[np.ndarray, list[Optional[float]], list[float]]],
|
||||
ylabel: str,
|
||||
xlabel: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
labels: Optional[list[str]] = None,
|
||||
markers: Optional[list[str]] = None,
|
||||
line_styles: Optional[list[str]] = None,
|
||||
x2label: Optional[Union[str, None]] = "Hours Since Start",
|
||||
) -> None:
|
||||
"""Create a line chart and add it to the current group."""
|
||||
|
||||
def chart() -> None:
|
||||
timestamps = [
|
||||
start_date.add(hours=i) for i in range(len(y_list[0]))
|
||||
] # 840 timestamps at 1-hour intervals
|
||||
|
||||
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 "-"
|
||||
plt.plot(
|
||||
timestamps, y_data, label=label, marker=marker, linestyle=line_style
|
||||
) # Plot line
|
||||
|
||||
# Format the time axis
|
||||
plt.gca().xaxis.set_major_formatter(
|
||||
mdates.DateFormatter("%Y-%m-%d", tz=self.config.general.timezone)
|
||||
) # Show date and time
|
||||
plt.gca().xaxis.set_major_locator(
|
||||
mdates.DayLocator(interval=1, tz=self.config.general.timezone)
|
||||
) # Major ticks every day
|
||||
plt.gca().xaxis.set_minor_locator(
|
||||
mdates.HourLocator(interval=2, tz=self.config.general.timezone)
|
||||
)
|
||||
# Minor ticks every 6 hours
|
||||
plt.gca().xaxis.set_minor_formatter(
|
||||
mdates.DateFormatter("%H", tz=self.config.general.timezone)
|
||||
)
|
||||
# plt.gcf().autofmt_xdate(rotation=45, which="major")
|
||||
# Auto-format the x-axis for readability
|
||||
|
||||
# Move major tick labels further down to avoid collision with minor tick labels
|
||||
for plt_label in plt.gca().get_xticklabels(which="major"):
|
||||
plt_label.set_y(-0.04)
|
||||
|
||||
# Add labels, title, and legend
|
||||
if xlabel:
|
||||
plt.xlabel(xlabel)
|
||||
plt.ylabel(ylabel)
|
||||
if title:
|
||||
plt.title(title)
|
||||
if labels:
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Add vertical line for the current date if within the axis range
|
||||
current_time = pendulum.now(self.config.general.timezone)
|
||||
if timestamps[0].subtract(hours=2) <= current_time <= timestamps[-1]:
|
||||
plt.axvline(current_time, color="r", linestyle="--", label="Now")
|
||||
plt.text(current_time, plt.ylim()[1], "Now", color="r", ha="center", va="bottom")
|
||||
|
||||
# Add a second x-axis on top
|
||||
ax1 = plt.gca()
|
||||
ax2 = ax1.twiny()
|
||||
ax2.set_xlim(ax1.get_xlim()) # Align the second axis with the first
|
||||
|
||||
# Generate integer hour labels
|
||||
hours_since_start = [(t - timestamps[0]).total_seconds() / 3600 for t in timestamps]
|
||||
# ax2.set_xticks(timestamps[::48]) # Set ticks every 12 hours
|
||||
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[::48]])
|
||||
# ax2.set_xticks(timestamps[:: len(timestamps) // 24]) # Select 10 evenly spaced ticks
|
||||
ax2.set_xticks(timestamps[:: len(timestamps) // 12]) # Select 10 evenly spaced ticks
|
||||
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 24]])
|
||||
ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 12]])
|
||||
if x2label:
|
||||
ax2.set_xlabel(x2label)
|
||||
|
||||
# Ensure ax1 and ax2 are aligned
|
||||
# assert ax1.get_xlim() == ax2.get_xlim(), "ax1 and ax2 are not aligned"
|
||||
|
||||
self.add_chart_to_group(chart, title) # Add chart function to current group
|
||||
|
||||
def create_line_chart(
|
||||
self,
|
||||
start_hour: Optional[int],
|
||||
y_list: list[Union[np.ndarray, list[Optional[float]], 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, title) # 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, title) # Add chart function to current group
|
||||
|
||||
def create_bar_chart(
|
||||
self,
|
||||
labels: list[str],
|
||||
values_list: Sequence[Sequence[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, title) # 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, title) # Add chart function to current group
|
||||
|
||||
def add_text_page(self, text: str, title: Optional[str] = None, fontsize: int = 12) -> None:
|
||||
"""Add a page with text content to the PDF."""
|
||||
|
||||
def chart() -> None:
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
fig = plt.figure(figsize=(8.5, 11)) # Create a standard page size
|
||||
plt.axis("off") # Turn off axes for a clean page
|
||||
wrapped_text = textwrap.fill(text, width=80) # Wrap text to fit the page width
|
||||
y = 0.95 # Start at the top of the page
|
||||
|
||||
if title:
|
||||
plt.text(0.5, y, title, ha="center", va="top", fontsize=fontsize + 4, weight="bold")
|
||||
y -= 0.05 # Add space after the title
|
||||
|
||||
plt.text(0.5, y, wrapped_text, ha="center", va="top", fontsize=fontsize, wrap=True)
|
||||
self.pdf_pages.savefig(fig) # Save the figure as a page in the PDF
|
||||
plt.close(fig) # Close the figure to free up memory
|
||||
|
||||
self.add_chart_to_group(chart, title) # Treat the text page as a "chart" in the group
|
||||
|
||||
def add_json_page(
|
||||
self, json_obj: dict, title: Optional[str] = None, fontsize: int = 12
|
||||
) -> None:
|
||||
"""Add a page with a formatted JSON object to the PDF.
|
||||
|
||||
Args:
|
||||
json_obj (dict): The JSON object to display.
|
||||
title (Optional[str]): An optional title for the page.
|
||||
fontsize (int): The font size for the JSON text.
|
||||
"""
|
||||
|
||||
def chart() -> None:
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
# Convert JSON object to a formatted string
|
||||
json_str = json.dumps(json_obj, indent=4)
|
||||
|
||||
fig = plt.figure(figsize=(8.5, 11)) # Standard page size
|
||||
plt.axis("off") # Turn off axes for a clean page
|
||||
|
||||
y = 0.95 # Start at the top of the page
|
||||
if title:
|
||||
plt.text(0.5, y, title, ha="center", va="top", fontsize=fontsize + 4, weight="bold")
|
||||
y -= 0.05 # Add space after the title
|
||||
|
||||
# Split the JSON string into lines and render them
|
||||
lines = json_str.splitlines()
|
||||
for line in lines:
|
||||
plt.text(0.05, y, line, ha="left", va="top", fontsize=fontsize, family="monospace")
|
||||
y -= 0.02 # Move down for the next line
|
||||
|
||||
# Stop if the text exceeds the page
|
||||
if y < 0.05:
|
||||
break
|
||||
|
||||
self.pdf_pages.savefig(fig) # Save the figure as a page in the PDF
|
||||
plt.close(fig) # Close the figure to free up memory
|
||||
|
||||
self.add_chart_to_group(chart, title) # Treat the JSON page as a "chart" in the group
|
||||
|
||||
def generate_pdf(self) -> bytes:
|
||||
"""Generate the PDF report with all the added chart groups.
|
||||
|
||||
Returns:
|
||||
PDF report as bytes object
|
||||
"""
|
||||
self._initialize_pdf() # Initialize the PDF
|
||||
|
||||
buffer = BytesIO()
|
||||
self.pdf_pages = PdfPages(buffer, metadata={})
|
||||
|
||||
for group in self.groups:
|
||||
self._save_group_to_pdf(group)
|
||||
|
||||
self.pdf_pages.close()
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def genetic_prepare_visualize(
|
||||
solution: GeneticSolution,
|
||||
) -> bytes:
|
||||
global debug_visualize
|
||||
|
||||
report = GeneticVisualizationReport()
|
||||
next_full_hour_date = get_ems().start_datetime
|
||||
start_hour = solution.start_hour
|
||||
# Group 1:
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.ems.total_load[start_hour:],
|
||||
],
|
||||
title="Load Profile",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="Load (Wh)",
|
||||
labels=["Total Load (Wh)"],
|
||||
)
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.ems.pv_forecast_wh[start_hour:],
|
||||
],
|
||||
title="PV Forecast",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="PV Generation (Wh)",
|
||||
)
|
||||
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
np.full(
|
||||
len(solution.parameters.ems.total_load) - start_hour,
|
||||
solution.parameters.ems.feed_in_tariff_per_wh[start_hour:]
|
||||
if isinstance(solution.parameters.ems.feed_in_tariff_per_wh, list)
|
||||
else solution.parameters.ems.feed_in_tariff_per_wh,
|
||||
)
|
||||
],
|
||||
title="Remuneration",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="amount/Wh",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
if solution.parameters.temperature_forecast:
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.temperature_forecast[start_hour:],
|
||||
],
|
||||
title="Temperature Forecast",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="°C",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Group 2:
|
||||
report.create_line_chart_date(
|
||||
start_date=next_full_hour_date, # start_date
|
||||
y_list=[
|
||||
solution.result.load_wh_per_hour,
|
||||
solution.result.home_appliance_wh_per_hour,
|
||||
solution.result.grid_feed_in_wh_per_hour,
|
||||
solution.result.grid_consumption_wh_per_hour,
|
||||
solution.result.losses_per_hour,
|
||||
],
|
||||
title="Energy Flow per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
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_date(
|
||||
start_date=next_full_hour_date,
|
||||
y_list=[solution.result.battery_soc_per_hour, solution.result.ev_soc_per_hour],
|
||||
title="Battery SOC",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="%",
|
||||
labels=[
|
||||
"Battery SOC (%)",
|
||||
"Electric Vehicle SOC (%)",
|
||||
],
|
||||
markers=["o", "x"],
|
||||
)
|
||||
report.create_line_chart_date(
|
||||
start_date=next_full_hour_date, # start_date
|
||||
y_list=[solution.parameters.ems.electricity_price_per_wh[start_hour:]],
|
||||
# title="Electricity Price", # not enough space
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Electricity Price (amount/Wh)",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
|
||||
labels = list(
|
||||
item
|
||||
for sublist in zip(
|
||||
list(str(i) for i in range(0, 23, 2)), list(str(" ") for i in range(0, 23, 2))
|
||||
)
|
||||
for item in sublist
|
||||
)
|
||||
labels = labels[start_hour:] + labels
|
||||
|
||||
report.create_bar_chart(
|
||||
labels=labels,
|
||||
values_list=[
|
||||
solution.ac_charge[start_hour:],
|
||||
solution.dc_charge[start_hour:],
|
||||
solution.discharge_allowed[start_hour:],
|
||||
],
|
||||
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,
|
||||
xlabels=labels,
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Group 4:
|
||||
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[
|
||||
solution.result.costs_per_hour,
|
||||
solution.result.revenue_per_hour,
|
||||
],
|
||||
title="Financial Balance per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Amount",
|
||||
labels=["Costs", "Revenue"],
|
||||
)
|
||||
|
||||
extra_data = solution.extra_data
|
||||
if extra_data:
|
||||
report.create_scatter_plot(
|
||||
x=np.array(extra_data["losses"]),
|
||||
y=np.array(extra_data["balance"]),
|
||||
title="Scatter Plot",
|
||||
xlabel="losses",
|
||||
ylabel="balance",
|
||||
c=np.array(extra_data["constraints"]),
|
||||
)
|
||||
|
||||
values_list = [
|
||||
[
|
||||
solution.result.total_costs,
|
||||
solution.result.total_revenue,
|
||||
solution.result.total_balance,
|
||||
]
|
||||
]
|
||||
labels = ["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"]
|
||||
|
||||
report.create_bar_chart(
|
||||
labels=labels,
|
||||
values_list=values_list,
|
||||
title="Financial Overview",
|
||||
ylabel="Amount",
|
||||
xlabels=["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"],
|
||||
)
|
||||
|
||||
report.finalize_group()
|
||||
|
||||
# Group 1: Scatter plot of losses vs balance with color-coded constraints
|
||||
if extra_data:
|
||||
f1 = np.array(extra_data["losses"]) # Losses
|
||||
f2 = np.array(extra_data["balance"]) # Balance
|
||||
n1 = np.array(extra_data["constraints"]) # Constraints
|
||||
|
||||
# Filter data where 'constraints' < 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 extra_data:
|
||||
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()
|
||||
|
||||
if solution.fitness_history:
|
||||
if debug_visualize or solution.fixed_seed:
|
||||
report.create_line_chart(
|
||||
start_hour=0,
|
||||
y_list=[
|
||||
np.array(solution.fitness_history["avg"]),
|
||||
np.array(solution.fitness_history["max"]),
|
||||
np.array(solution.fitness_history["min"]),
|
||||
],
|
||||
title=f"DEBUG: Generation Fitness for seed {solution.fixed_seed}",
|
||||
xlabel="Generation",
|
||||
ylabel="Fitness",
|
||||
labels=[
|
||||
"avg",
|
||||
"max",
|
||||
"min",
|
||||
],
|
||||
markers=[".", ".", "."],
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Generate the PDF report
|
||||
pdf = report.generate_pdf()
|
||||
|
||||
# Return pdf (a bytes object)
|
||||
return pdf
|
||||
|
||||
|
||||
def genetic_generate_example_report(filename: str = "example_report.pdf") -> None:
|
||||
"""Generate example visualization report."""
|
||||
global debug_visualize
|
||||
|
||||
report = GeneticVisualizationReport("test")
|
||||
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
|
||||
|
||||
debug_visualize = True # set level for example report
|
||||
|
||||
if debug_visualize:
|
||||
report.create_line_chart(
|
||||
x_hours,
|
||||
[np.array([0.2, 0.25, 0.3, 0.35])],
|
||||
title="DEBUG",
|
||||
xlabel="DEBUG",
|
||||
ylabel="DEBUG",
|
||||
)
|
||||
report.finalize_group() # Finalize the third group of charts
|
||||
|
||||
report.add_text_page(
|
||||
text=" Bisher passierte folgendes:"
|
||||
"Am Anfang wurde das Universum erschaffen."
|
||||
"Das machte viele Leute sehr wütend und wurde allent-"
|
||||
"halben als Schritt in die falsche Richtung angesehen...",
|
||||
title="Don't Panic!",
|
||||
fontsize=14,
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
sample_json = {
|
||||
"name": "Visualization Report",
|
||||
"version": 1.0,
|
||||
"charts": [
|
||||
{"type": "line", "data_points": 50},
|
||||
{"type": "bar", "categories": 10},
|
||||
],
|
||||
"metadata": {"author": "AI Assistant", "date": "2025-01-11"},
|
||||
}
|
||||
|
||||
report.add_json_page(json_obj=sample_json, title="Formatted JSON Data", fontsize=10)
|
||||
report.finalize_group()
|
||||
|
||||
report.create_line_chart_date(
|
||||
pendulum.now().subtract(hours=0),
|
||||
[list(np.random.random(840))],
|
||||
title="test",
|
||||
xlabel="test",
|
||||
ylabel="test",
|
||||
)
|
||||
report.finalize_group()
|
||||
# Generate the PDF report (a bytes object)
|
||||
pdf = report.generate_pdf()
|
||||
|
||||
# write to file
|
||||
with open(filename, "wb") as f:
|
||||
f.write(pdf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
genetic_generate_example_report()
|
||||
@@ -1067,7 +1067,7 @@ class Genetic0Optimization(OptimizationBase):
|
||||
# Start hour has to be in sync with energy management
|
||||
if start_hour != self.ems.start_datetime.hour:
|
||||
raise ValueError(
|
||||
f"Start hour not synced. EMS {self.ems.start_datetime.hour} vs. GENETIC {start_hour}."
|
||||
f"Start hour not synced. EMS {self.ems.start_datetime.hour} vs. GENETIC0 {start_hour}."
|
||||
)
|
||||
|
||||
# Set the number of generations
|
||||
@@ -1224,39 +1224,20 @@ class Genetic0Optimization(OptimizationBase):
|
||||
else:
|
||||
discharge = discharge.tolist()
|
||||
|
||||
# Visualize the results in PDF
|
||||
try:
|
||||
from akkudoktoreos.utils.visualize import prepare_visualize
|
||||
|
||||
visualize = {
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"ev_charge_hours_float": ev_charge_hours_float,
|
||||
"result": Genetic0SimulationResult(**simulation_result).model_dump(),
|
||||
"ev_obj": self.simulation.ev.to_dict() if self.simulation.ev else None,
|
||||
"start_solution": start_solution,
|
||||
"washingstart": washingstart_int,
|
||||
"extra_data": extra_data,
|
||||
"fitness_history": self.fitness_history,
|
||||
"fixed_seed": self.fix_seed,
|
||||
}
|
||||
|
||||
prepare_visualize(parameters, visualize, start_hour=start_hour)
|
||||
|
||||
except Exception as ex:
|
||||
error_msg = f"Visualization failed: {ex}"
|
||||
logger.error(error_msg)
|
||||
|
||||
return Genetic0Solution(
|
||||
**{
|
||||
"parameters": parameters,
|
||||
"ac_charge": ac_charge_hours,
|
||||
"dc_charge": dc_charge_hours,
|
||||
"discharge_allowed": discharge,
|
||||
"ev_charge_hours_float": ev_charge_hours_float,
|
||||
"result": Genetic0SimulationResult(**simulation_result),
|
||||
"ev_obj": self.simulation.ev,
|
||||
"start_hour": start_hour,
|
||||
"start_solution": start_solution,
|
||||
"washingstart": washingstart_int,
|
||||
"extra_data": extra_data,
|
||||
"fitness_history": self.fitness_history,
|
||||
"fixed_seed": self.fix_seed,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Genetic0 algorithm optimisation solution."""
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -29,6 +29,9 @@ from akkudoktoreos.devices.genetic0.genetic0battery import Genetic0Battery
|
||||
from akkudoktoreos.optimization.genetic0.genetic0devices import (
|
||||
Genetic0ParametersBaseModel,
|
||||
)
|
||||
from akkudoktoreos.optimization.genetic0.genetic0params import (
|
||||
Genetic0OptimizationParameters,
|
||||
)
|
||||
from akkudoktoreos.optimization.optimization import OptimizationSolution
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
|
||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
@@ -244,8 +247,9 @@ class Genetic0SimulationResult(Genetic0ParametersBaseModel):
|
||||
return NumpyEncoder.convert_numpy(field)[0]
|
||||
|
||||
|
||||
class Genetic0Solution(ConfigMixin, Genetic0ParametersBaseModel):
|
||||
"""**Note**: The first value of "load_wh_per_hour", "grid_feed_in_wh_per_hour", and "grid_consumption_wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged."""
|
||||
# Legacy GENETIC solution dataset. To keep the /optimize endpoint compatible.
|
||||
class Genetic0SolutionLegacy(Genetic0ParametersBaseModel):
|
||||
"""GENETIC0 solution legacy dataset."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
||||
|
||||
@@ -317,6 +321,109 @@ class Genetic0Solution(ConfigMixin, Genetic0ParametersBaseModel):
|
||||
return Genetic0ElectricVehicleResult(**field.to_dict())
|
||||
return field
|
||||
|
||||
|
||||
class Genetic0Solution(ConfigMixin, Genetic0ParametersBaseModel):
|
||||
"""**Note**: The first value of "load_wh_per_hour", "grid_feed_in_wh_per_hour", and "grid_consumption_wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
||||
|
||||
parameters: Genetic0OptimizationParameters = Field(
|
||||
json_schema_extra={"description": "Optimization parameters used to generate solution."}
|
||||
)
|
||||
ac_charge: list[float] = Field(
|
||||
json_schema_extra={
|
||||
"description": "Array with AC charging values as relative power (0.0-1.0), other values set to 0."
|
||||
}
|
||||
)
|
||||
dc_charge: list[float] = Field(
|
||||
json_schema_extra={
|
||||
"description": "Array with DC charging values as relative power (0-1), other values set to 0."
|
||||
}
|
||||
)
|
||||
discharge_allowed: list[int] = Field(
|
||||
json_schema_extra={
|
||||
"description": "Array with discharge values (1 for discharge, 0 otherwise)."
|
||||
}
|
||||
)
|
||||
ev_charge_hours_float: Optional[list[float]] = Field(
|
||||
validation_alias=AliasChoices("ev_charge_hours_float", "eautocharge_hours_float"),
|
||||
json_schema_extra={
|
||||
"description": "Array with EV charging values as relative power (0.0-1.0), or `null` if no EV is optimized."
|
||||
},
|
||||
)
|
||||
result: Genetic0SimulationResult
|
||||
ev_obj: Optional[Genetic0ElectricVehicleResult] = Field(
|
||||
validation_alias=AliasChoices("ev_obj", "eauto_obj"),
|
||||
json_schema_extra={"description": "Electric vehicle state after optimization."},
|
||||
)
|
||||
start_hour: int = Field(
|
||||
default=0,
|
||||
json_schema_extra={"description": "Start hour."},
|
||||
)
|
||||
start_solution: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": "An array of binary values (0 or 1) representing a possible starting solution for the simulation."
|
||||
},
|
||||
)
|
||||
washingstart: Optional[int] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": "Can be `null` or contain an object representing the start of washing (if applicable)."
|
||||
},
|
||||
)
|
||||
extra_data: Optional[dict[str, Union[list[int], list[float]]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": ("Dictionary of balance: TBD, losses: TBD, constraints: TBD.")
|
||||
},
|
||||
)
|
||||
fitness_history: Optional[dict[str, Union[list[int], list[float]]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Dictionary of "
|
||||
"gen: Generation numbers (X-axis), "
|
||||
"avg: Average fitness for each generation (Y-axis), "
|
||||
"max: Maximum fitness for each generation (Y-axis), "
|
||||
"min: Minimum fitness for each generation (Y-axis)."
|
||||
)
|
||||
},
|
||||
)
|
||||
fixed_seed: Optional[int] = Field(
|
||||
default=None,
|
||||
json_schema_extra={"description": "Fixed seed."},
|
||||
)
|
||||
|
||||
# Computed fields for backward compatibility (deprecated German names)
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def eautocharge_hours_float(self) -> Optional[list[float]]:
|
||||
"""Deprecated: Use ev_charge_hours_float instead."""
|
||||
return self.ev_charge_hours_float
|
||||
|
||||
@computed_field(json_schema_extra={"deprecated": True})
|
||||
def eauto_obj(self) -> Optional[Genetic0ElectricVehicleResult]:
|
||||
"""Deprecated: Use ev_obj instead."""
|
||||
return self.ev_obj
|
||||
|
||||
@field_validator(
|
||||
"ac_charge",
|
||||
"dc_charge",
|
||||
"discharge_allowed",
|
||||
mode="before",
|
||||
)
|
||||
def convert_numpy(cls, field: Any) -> Any:
|
||||
return NumpyEncoder.convert_numpy(field)[0]
|
||||
|
||||
@field_validator(
|
||||
"ev_obj",
|
||||
mode="before",
|
||||
)
|
||||
def convert_eauto(cls, field: Any) -> Any:
|
||||
if isinstance(field, Genetic0Battery):
|
||||
return Genetic0ElectricVehicleResult(**field.to_dict())
|
||||
return field
|
||||
|
||||
def _battery_device_id(self) -> str:
|
||||
"""Get battery device id."""
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,801 @@
|
||||
import json
|
||||
import textwrap
|
||||
from collections.abc import Sequence
|
||||
from io import BytesIO
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pendulum
|
||||
from matplotlib.backends.backend_pdf import PdfPages
|
||||
|
||||
from akkudoktoreos.core.coreabc import ConfigMixin, get_ems
|
||||
from akkudoktoreos.optimization.genetic0.genetic0solution import Genetic0Solution
|
||||
from akkudoktoreos.utils.datetimeutil import DateTime, to_datetime
|
||||
|
||||
matplotlib.use(
|
||||
"Agg"
|
||||
) # non-interactive backend that can only write to files, backend needed to stay in main thread.
|
||||
|
||||
|
||||
debug_visualize: bool = False
|
||||
|
||||
|
||||
class Genetic0VisualizationReport(ConfigMixin):
|
||||
def __init__(
|
||||
self,
|
||||
version: str = "0.0.1",
|
||||
) -> None:
|
||||
# Initialize the report with empty groups
|
||||
self.groups: list[list[Callable[[], None]]] = [] # Store groups of charts
|
||||
self.current_group: list[
|
||||
Callable[[], None]
|
||||
] = [] # Store current group of charts being created
|
||||
|
||||
# file like buffer to create the PDF
|
||||
self._buffer = BytesIO()
|
||||
self.pdf_pages: PdfPages | None = None
|
||||
|
||||
self.version = version # overwrite version as test for constant output of pdf for test
|
||||
self.current_time = to_datetime(
|
||||
as_string="YYYY-MM-DD HH:mm:ss", in_timezone=self.config.general.timezone
|
||||
)
|
||||
|
||||
def add_chart_to_group(self, chart_func: Callable[[], None], title: str | None) -> None:
|
||||
"""Add a chart function to the current group and save it as a PNG and SVG."""
|
||||
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:
|
||||
"""Initialize the PDF."""
|
||||
self._buffer = BytesIO()
|
||||
self.pdf_pages = PdfPages(self._buffer, metadata={}) # Initialize PdfPages without metadata
|
||||
|
||||
def _save_group_to_pdf(self, group: list[Callable[[], None]]) -> None:
|
||||
"""Save a group of charts to the PDF."""
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
fig_count = len(group) # Number of charts in the group
|
||||
|
||||
if fig_count == 0:
|
||||
print("Attempted to save an empty group to PDF!")
|
||||
return
|
||||
|
||||
# Check for special charts before creating layout
|
||||
special_keywords = {"add_text_page", "add_json_page"}
|
||||
for chart_func in group:
|
||||
if any(keyword in chart_func.__qualname__ for keyword in special_keywords):
|
||||
chart_func() # Special chart functions handle their own rendering
|
||||
return
|
||||
|
||||
# Create layout only if no special charts are detected
|
||||
if fig_count == 3:
|
||||
fig = plt.figure(figsize=(14, 10))
|
||||
ax1 = fig.add_subplot(2, 1, 1)
|
||||
ax2 = fig.add_subplot(2, 2, 3)
|
||||
ax3 = fig.add_subplot(2, 2, 4)
|
||||
axs = [ax1, ax2, ax3]
|
||||
else:
|
||||
cols = 2 if fig_count > 1 else 1
|
||||
rows = (fig_count + 1) // 2
|
||||
fig, axs = plt.subplots(rows, cols, figsize=(14, 7 * rows))
|
||||
axs = list(np.array(axs).reshape(-1))
|
||||
|
||||
# Add footer text with current time to each page
|
||||
if self.version == "test":
|
||||
current_time = "test"
|
||||
else:
|
||||
current_time = self.current_time
|
||||
fig.text(
|
||||
0.5,
|
||||
0.02,
|
||||
f"Generated on: {current_time} with version: {self.version}",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=10,
|
||||
)
|
||||
|
||||
# Render each chart in its corresponding axis
|
||||
for idx, chart_func in enumerate(group):
|
||||
plt.sca(axs[idx]) # Set current axis
|
||||
chart_func() # Render the chart
|
||||
|
||||
# Save the figure to the PDF and clean up
|
||||
for idx in range(fig_count, len(axs)):
|
||||
axs[idx].set_visible(False)
|
||||
|
||||
self.pdf_pages.savefig(fig) # Save the figure to the PDF
|
||||
plt.close(fig)
|
||||
|
||||
def create_line_chart_date(
|
||||
self,
|
||||
start_date: DateTime,
|
||||
y_list: list[Union[np.ndarray, list[Optional[float]], list[float]]],
|
||||
ylabel: str,
|
||||
xlabel: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
labels: Optional[list[str]] = None,
|
||||
markers: Optional[list[str]] = None,
|
||||
line_styles: Optional[list[str]] = None,
|
||||
x2label: Optional[Union[str, None]] = "Hours Since Start",
|
||||
) -> None:
|
||||
"""Create a line chart and add it to the current group."""
|
||||
|
||||
def chart() -> None:
|
||||
timestamps = [
|
||||
start_date.add(hours=i) for i in range(len(y_list[0]))
|
||||
] # 840 timestamps at 1-hour intervals
|
||||
|
||||
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 "-"
|
||||
plt.plot(
|
||||
timestamps, y_data, label=label, marker=marker, linestyle=line_style
|
||||
) # Plot line
|
||||
|
||||
# Format the time axis
|
||||
plt.gca().xaxis.set_major_formatter(
|
||||
mdates.DateFormatter("%Y-%m-%d", tz=self.config.general.timezone)
|
||||
) # Show date and time
|
||||
plt.gca().xaxis.set_major_locator(
|
||||
mdates.DayLocator(interval=1, tz=self.config.general.timezone)
|
||||
) # Major ticks every day
|
||||
plt.gca().xaxis.set_minor_locator(
|
||||
mdates.HourLocator(interval=2, tz=self.config.general.timezone)
|
||||
)
|
||||
# Minor ticks every 6 hours
|
||||
plt.gca().xaxis.set_minor_formatter(
|
||||
mdates.DateFormatter("%H", tz=self.config.general.timezone)
|
||||
)
|
||||
# plt.gcf().autofmt_xdate(rotation=45, which="major")
|
||||
# Auto-format the x-axis for readability
|
||||
|
||||
# Move major tick labels further down to avoid collision with minor tick labels
|
||||
for plt_label in plt.gca().get_xticklabels(which="major"):
|
||||
plt_label.set_y(-0.04)
|
||||
|
||||
# Add labels, title, and legend
|
||||
if xlabel:
|
||||
plt.xlabel(xlabel)
|
||||
plt.ylabel(ylabel)
|
||||
if title:
|
||||
plt.title(title)
|
||||
if labels:
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Add vertical line for the current date if within the axis range
|
||||
current_time = pendulum.now(self.config.general.timezone)
|
||||
if timestamps[0].subtract(hours=2) <= current_time <= timestamps[-1]:
|
||||
plt.axvline(current_time, color="r", linestyle="--", label="Now")
|
||||
plt.text(current_time, plt.ylim()[1], "Now", color="r", ha="center", va="bottom")
|
||||
|
||||
# Add a second x-axis on top
|
||||
ax1 = plt.gca()
|
||||
ax2 = ax1.twiny()
|
||||
ax2.set_xlim(ax1.get_xlim()) # Align the second axis with the first
|
||||
|
||||
# Generate integer hour labels
|
||||
hours_since_start = [(t - timestamps[0]).total_seconds() / 3600 for t in timestamps]
|
||||
# ax2.set_xticks(timestamps[::48]) # Set ticks every 12 hours
|
||||
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[::48]])
|
||||
# ax2.set_xticks(timestamps[:: len(timestamps) // 24]) # Select 10 evenly spaced ticks
|
||||
ax2.set_xticks(timestamps[:: len(timestamps) // 12]) # Select 10 evenly spaced ticks
|
||||
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 24]])
|
||||
ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 12]])
|
||||
if x2label:
|
||||
ax2.set_xlabel(x2label)
|
||||
|
||||
# Ensure ax1 and ax2 are aligned
|
||||
# assert ax1.get_xlim() == ax2.get_xlim(), "ax1 and ax2 are not aligned"
|
||||
|
||||
self.add_chart_to_group(chart, title) # Add chart function to current group
|
||||
|
||||
def create_line_chart(
|
||||
self,
|
||||
start_hour: Optional[int],
|
||||
y_list: list[Union[np.ndarray, list[Optional[float]], 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, title) # 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, title) # Add chart function to current group
|
||||
|
||||
def create_bar_chart(
|
||||
self,
|
||||
labels: Sequence[str],
|
||||
values_list: Sequence[Sequence[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, title) # 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, title) # Add chart function to current group
|
||||
|
||||
def add_text_page(self, text: str, title: Optional[str] = None, fontsize: int = 12) -> None:
|
||||
"""Add a page with text content to the PDF."""
|
||||
|
||||
def chart() -> None:
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
fig = plt.figure(figsize=(8.5, 11)) # Create a standard page size
|
||||
plt.axis("off") # Turn off axes for a clean page
|
||||
wrapped_text = textwrap.fill(text, width=80) # Wrap text to fit the page width
|
||||
y = 0.95 # Start at the top of the page
|
||||
|
||||
if title:
|
||||
plt.text(0.5, y, title, ha="center", va="top", fontsize=fontsize + 4, weight="bold")
|
||||
y -= 0.05 # Add space after the title
|
||||
|
||||
plt.text(0.5, y, wrapped_text, ha="center", va="top", fontsize=fontsize, wrap=True)
|
||||
self.pdf_pages.savefig(fig) # Save the figure as a page in the PDF
|
||||
plt.close(fig) # Close the figure to free up memory
|
||||
|
||||
self.add_chart_to_group(chart, title) # Treat the text page as a "chart" in the group
|
||||
|
||||
def add_json_page(
|
||||
self, json_obj: dict, title: Optional[str] = None, fontsize: int = 12
|
||||
) -> None:
|
||||
"""Add a page with a formatted JSON object to the PDF.
|
||||
|
||||
Args:
|
||||
json_obj (dict): The JSON object to display.
|
||||
title (Optional[str]): An optional title for the page.
|
||||
fontsize (int): The font size for the JSON text.
|
||||
"""
|
||||
|
||||
def chart() -> None:
|
||||
if self.pdf_pages is None: # make mypy happy
|
||||
raise RuntimeError("Report not initialized")
|
||||
|
||||
# Convert JSON object to a formatted string
|
||||
json_str = json.dumps(json_obj, indent=4)
|
||||
|
||||
fig = plt.figure(figsize=(8.5, 11)) # Standard page size
|
||||
plt.axis("off") # Turn off axes for a clean page
|
||||
|
||||
y = 0.95 # Start at the top of the page
|
||||
if title:
|
||||
plt.text(0.5, y, title, ha="center", va="top", fontsize=fontsize + 4, weight="bold")
|
||||
y -= 0.05 # Add space after the title
|
||||
|
||||
# Split the JSON string into lines and render them
|
||||
lines = json_str.splitlines()
|
||||
for line in lines:
|
||||
plt.text(0.05, y, line, ha="left", va="top", fontsize=fontsize, family="monospace")
|
||||
y -= 0.02 # Move down for the next line
|
||||
|
||||
# Stop if the text exceeds the page
|
||||
if y < 0.05:
|
||||
break
|
||||
|
||||
self.pdf_pages.savefig(fig) # Save the figure as a page in the PDF
|
||||
plt.close(fig) # Close the figure to free up memory
|
||||
|
||||
self.add_chart_to_group(chart, title) # Treat the JSON page as a "chart" in the group
|
||||
|
||||
def generate_pdf(self) -> bytes:
|
||||
"""Generate the PDF report with all the added chart groups.
|
||||
|
||||
Returns:
|
||||
PDF report as bytes object
|
||||
"""
|
||||
self._initialize_pdf() # Initialize the PDF
|
||||
|
||||
buffer = BytesIO()
|
||||
self.pdf_pages = PdfPages(buffer, metadata={})
|
||||
|
||||
for group in self.groups:
|
||||
self._save_group_to_pdf(group)
|
||||
|
||||
self.pdf_pages.close()
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def genetic0_prepare_visualize(
|
||||
solution: Genetic0Solution,
|
||||
) -> bytes:
|
||||
global debug_visualize
|
||||
|
||||
report = Genetic0VisualizationReport()
|
||||
next_full_hour_date = get_ems().start_datetime
|
||||
start_hour = solution.start_hour
|
||||
# Group 1:
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.ems.total_load[start_hour:],
|
||||
],
|
||||
title="Load Profile",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="Load (Wh)",
|
||||
labels=["Total Load (Wh)"],
|
||||
)
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.ems.pv_forecast_wh[start_hour:],
|
||||
],
|
||||
title="PV Forecast",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="PV Generation (Wh)",
|
||||
)
|
||||
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
np.full(
|
||||
len(solution.parameters.ems.total_load) - start_hour,
|
||||
solution.parameters.ems.feed_in_tariff_per_wh[start_hour:]
|
||||
if isinstance(solution.parameters.ems.feed_in_tariff_per_wh, list)
|
||||
else solution.parameters.ems.feed_in_tariff_per_wh,
|
||||
)
|
||||
],
|
||||
title="Remuneration",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="amount/Wh",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
if solution.parameters.temperature_forecast:
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date,
|
||||
[
|
||||
solution.parameters.temperature_forecast[start_hour:],
|
||||
],
|
||||
title="Temperature Forecast",
|
||||
# xlabel="Hours", # not enough space
|
||||
ylabel="°C",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Group 2:
|
||||
report.create_line_chart_date(
|
||||
start_date=next_full_hour_date, # start_date
|
||||
y_list=[
|
||||
solution.result.load_wh_per_hour,
|
||||
solution.result.home_appliance_wh_per_hour,
|
||||
solution.result.grid_feed_in_wh_per_hour,
|
||||
solution.result.grid_consumption_wh_per_hour,
|
||||
solution.result.losses_per_hour,
|
||||
],
|
||||
title="Energy Flow per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
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_date(
|
||||
start_date=next_full_hour_date,
|
||||
y_list=[solution.result.battery_soc_per_hour, solution.result.ev_soc_per_hour],
|
||||
title="Battery SOC",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="%",
|
||||
labels=[
|
||||
"Battery SOC (%)",
|
||||
"Electric Vehicle SOC (%)",
|
||||
],
|
||||
markers=["o", "x"],
|
||||
)
|
||||
report.create_line_chart_date(
|
||||
start_date=next_full_hour_date, # start_date
|
||||
y_list=[solution.parameters.ems.electricity_price_per_wh[start_hour:]],
|
||||
# title="Electricity Price", # not enough space
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Electricity Price (amount/Wh)",
|
||||
x2label=None, # not enough space
|
||||
)
|
||||
|
||||
labels = list(
|
||||
item
|
||||
for sublist in zip(
|
||||
list(str(i) for i in range(0, 23, 2)), list(str(" ") for i in range(0, 23, 2))
|
||||
)
|
||||
for item in sublist
|
||||
)
|
||||
labels = labels[start_hour:] + labels
|
||||
|
||||
report.create_bar_chart(
|
||||
labels=labels,
|
||||
values_list=[
|
||||
solution.ac_charge[start_hour:],
|
||||
solution.dc_charge[start_hour:],
|
||||
solution.discharge_allowed[start_hour:],
|
||||
],
|
||||
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,
|
||||
xlabels=labels,
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Group 4:
|
||||
|
||||
report.create_line_chart_date(
|
||||
next_full_hour_date, # start_date
|
||||
[
|
||||
solution.result.costs_per_hour,
|
||||
solution.result.revenue_per_hour,
|
||||
],
|
||||
title="Financial Balance per Hour",
|
||||
# xlabel="Date", # not enough space
|
||||
ylabel="Amount",
|
||||
labels=["Costs", "Revenue"],
|
||||
)
|
||||
|
||||
extra_data = solution.extra_data
|
||||
if extra_data:
|
||||
report.create_scatter_plot(
|
||||
x=np.array(extra_data["losses"]),
|
||||
y=np.array(extra_data["balance"]),
|
||||
title="Scatter Plot",
|
||||
xlabel="losses",
|
||||
ylabel="balance",
|
||||
c=np.array(extra_data["constraints"]),
|
||||
)
|
||||
|
||||
values_list = [
|
||||
[
|
||||
solution.result.total_costs,
|
||||
solution.result.total_revenue,
|
||||
solution.result.total_balance,
|
||||
]
|
||||
]
|
||||
labels = ["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"]
|
||||
|
||||
report.create_bar_chart(
|
||||
labels=labels,
|
||||
values_list=values_list,
|
||||
title="Financial Overview",
|
||||
ylabel="Amount",
|
||||
xlabels=["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"],
|
||||
)
|
||||
|
||||
report.finalize_group()
|
||||
|
||||
# Group 1: Scatter plot of losses vs balance with color-coded constraints
|
||||
if extra_data:
|
||||
f1 = np.array(extra_data["losses"]) # Losses
|
||||
f2 = np.array(extra_data["balance"]) # Balance
|
||||
n1 = np.array(extra_data["constraints"]) # Constraints
|
||||
|
||||
# Filter data where 'constraints' < 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 extra_data:
|
||||
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()
|
||||
|
||||
if solution.fitness_history:
|
||||
if debug_visualize or solution.fixed_seed:
|
||||
report.create_line_chart(
|
||||
start_hour=0,
|
||||
y_list=[
|
||||
np.array(solution.fitness_history["avg"]),
|
||||
np.array(solution.fitness_history["max"]),
|
||||
np.array(solution.fitness_history["min"]),
|
||||
],
|
||||
title=f"DEBUG: Generation Fitness for seed {solution.fixed_seed}",
|
||||
xlabel="Generation",
|
||||
ylabel="Fitness",
|
||||
labels=[
|
||||
"avg",
|
||||
"max",
|
||||
"min",
|
||||
],
|
||||
markers=[".", ".", "."],
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
# Generate the PDF report
|
||||
pdf = report.generate_pdf()
|
||||
|
||||
# Return pdf (a bytes object)
|
||||
return pdf
|
||||
|
||||
|
||||
def genetic0_generate_example_report(filename: str = "example_report.pdf") -> None:
|
||||
"""Generate example visualization report."""
|
||||
global debug_visualize
|
||||
|
||||
report = Genetic0VisualizationReport("test")
|
||||
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
|
||||
|
||||
debug_visualize = True # set level for example report
|
||||
|
||||
if debug_visualize:
|
||||
report.create_line_chart(
|
||||
x_hours,
|
||||
[np.array([0.2, 0.25, 0.3, 0.35])],
|
||||
title="DEBUG",
|
||||
xlabel="DEBUG",
|
||||
ylabel="DEBUG",
|
||||
)
|
||||
report.finalize_group() # Finalize the third group of charts
|
||||
|
||||
report.add_text_page(
|
||||
text=" Bisher passierte folgendes:"
|
||||
"Am Anfang wurde das Universum erschaffen."
|
||||
"Das machte viele Leute sehr wütend und wurde allent-"
|
||||
"halben als Schritt in die falsche Richtung angesehen...",
|
||||
title="Don't Panic!",
|
||||
fontsize=14,
|
||||
)
|
||||
report.finalize_group()
|
||||
|
||||
sample_json = {
|
||||
"name": "Visualization Report",
|
||||
"version": 1.0,
|
||||
"charts": [
|
||||
{"type": "line", "data_points": 50},
|
||||
{"type": "bar", "categories": 10},
|
||||
],
|
||||
"metadata": {"author": "AI Assistant", "date": "2025-01-11"},
|
||||
}
|
||||
|
||||
report.add_json_page(json_obj=sample_json, title="Formatted JSON Data", fontsize=10)
|
||||
report.finalize_group()
|
||||
|
||||
report.create_line_chart_date(
|
||||
pendulum.now().subtract(hours=0),
|
||||
[list(np.random.random(840))],
|
||||
title="test",
|
||||
xlabel="test",
|
||||
ylabel="test",
|
||||
)
|
||||
report.finalize_group()
|
||||
# Generate the PDF report (a bytes object)
|
||||
pdf = report.generate_pdf()
|
||||
|
||||
# write to file
|
||||
with open(filename, "wb") as f:
|
||||
f.write(pdf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
genetic0_generate_example_report()
|
||||
Reference in New Issue
Block a user