"""
Results Tab Module
==================
:author: Dipl.-Ing. (FH) Jonas Pfeiffer
Displaying results of energy system calculations with diagrams and tables, including stack plots, pie charts, and result tables.
"""
import sys
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QTableWidget, QTableWidgetItem,
QHeaderView, QScrollArea, QCheckBox, QApplication)
from PyQt6.QtCore import pyqtSignal
from districtheatingsim.gui.EnergySystemTab._10_utilities import CheckableComboBox, CollapsibleHeader
[docs]
class ResultsTab(QWidget):
"""
A QWidget subclass representing the ResultsTab.
Attributes:
data_added (pyqtSignal): A signal that emits data as an object.
data_manager (DataManager): An instance of the DataManager class for managing data.
parent (QWidget): The parent widget.
results (dict): A dictionary to store results.
selected_variables (list): A list of selected variables for plotting.
"""
data_added = pyqtSignal(object) # Signal, das Daten als Objekt überträgt
[docs]
def __init__(self, data_manager, parent=None):
"""
Initializes the ResultsTab.
:param data_manager: The data manager
:type data_manager: DataManager
:param parent: The parent widget
:type parent: QWidget or None
"""
super().__init__(parent)
self.data_manager = data_manager
self.parent = parent
self.results = {}
self.selected_variables = []
self.energy_system = None
self.data_manager.project_folder_changed.connect(self.updateDefaultPath)
self.updateDefaultPath(self.data_manager.variant_folder)
self.initUI()
[docs]
def updateDefaultPath(self, new_base_path):
"""
Updates the default base path.
:param new_base_path: The new base path
:type new_base_path: str
"""
self.base_path = new_base_path
[docs]
def initUI(self):
"""
Initializes the UI components of the ResultsTab.
"""
self.mainLayout = QVBoxLayout(self)
self.scrollArea = QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollWidget = QWidget()
self.scrollLayout = QVBoxLayout(self.scrollWidget)
self.setupDiagrams()
self.setupCollapsibleResultsSections()
self.scrollArea.setWidget(self.scrollWidget)
self.mainLayout.addWidget(self.scrollArea)
self.setLayout(self.mainLayout)
[docs]
def setupDiagrams(self):
"""
Sets up the collapsible diagrams for the ResultsTab.
"""
# Layout for variable selection (ComboBox and Checkbox)
self.variableSelectionLayout = QHBoxLayout()
self.variableComboBox = CheckableComboBox()
self.variableComboBox.view().pressed.connect(self.updateSelectedVariables)
self.secondYAxisCheckBox = QCheckBox("Second y-Axis")
self.secondYAxisCheckBox.stateChanged.connect(self.updateSelectedVariables)
self.variableSelectionLayout.addWidget(self.variableComboBox)
self.variableSelectionLayout.addWidget(self.secondYAxisCheckBox)
# First Diagram (Stackplot and Line Plot)
self.stackPlotFigure = Figure(figsize=(8, 6))
self.stackPlotCanvas = FigureCanvas(self.stackPlotFigure)
self.stackPlotCanvas.setMinimumSize(500, 500)
self.toolbar1 = NavigationToolbar(self.stackPlotCanvas, self)
self.diagram1_widget = QWidget()
diagram1_layout = QVBoxLayout(self.diagram1_widget)
diagram1_layout.addLayout(self.variableSelectionLayout) # Add the ComboBox and Checkbox layout
diagram1_layout.addWidget(self.stackPlotCanvas)
diagram1_layout.addWidget(self.toolbar1)
self.diagram1_section = CollapsibleHeader("Jahresganglinie Diagramm", self.diagram1_widget)
self.scrollLayout.addWidget(self.diagram1_section)
# Second Diagram (Pie Chart)
self.pieChartFigure = Figure(figsize=(6, 6))
self.pieChartCanvas = FigureCanvas(self.pieChartFigure)
self.pieChartCanvas.setMinimumSize(500, 500)
self.pieCharttoolbar = NavigationToolbar(self.pieChartCanvas, self)
self.diagram2_widget = QWidget()
diagram2_layout = QVBoxLayout(self.diagram2_widget)
diagram2_layout.addWidget(self.pieChartCanvas)
diagram2_layout.addWidget(self.pieCharttoolbar)
self.diagram2_section = CollapsibleHeader("Anteile Wärmeerzeugung Diagramm", self.diagram2_widget)
self.scrollLayout.addWidget(self.diagram2_section)
[docs]
def setupCollapsibleResultsSections(self):
"""
Sets up the collapsible sections for displaying results tables.
"""
# First Table (Results Table)
self.setupResultsTable()
self.table1_widget = QWidget()
table1_layout = QVBoxLayout(self.table1_widget)
table1_layout.addWidget(self.resultsTable)
self.table1_section = CollapsibleHeader("Ergebnisse Erzeugung", self.table1_widget)
self.scrollLayout.addWidget(self.table1_section)
# Second Table (Additional Results Table)
self.setupAdditionalResultsTable()
self.table2_widget = QWidget()
table2_layout = QVBoxLayout(self.table2_widget)
table2_layout.addWidget(self.additionalResultsTable)
self.table2_section = CollapsibleHeader("Ergebnisse Wirtschaftlichkeit", self.table2_widget)
self.scrollLayout.addWidget(self.table2_section)
[docs]
def addLabel(self, text):
"""
Adds a label to the layout.
:param text: The text for the label
:type text: str
"""
label = QLabel(text)
self.scrollLayout.addWidget(label)
[docs]
def setupResultsTable(self):
"""
Sets up the results table with additional columns for operational hours and starts.
"""
self.resultsTable = QTableWidget()
self.resultsTable.setColumnCount(9) # Updated to include new columns
self.resultsTable.setHorizontalHeaderLabels([
'Technologie', 'Wärmemenge (MWh)', 'Anzahl Betriebsstunden',
'Anzahl Starts', 'Betriebsstunden/Start', 'Kosten (€/MWh)',
'Anteil (%)', 'CO2-eq (t_CO2/MWh_th)', 'Primärenergiefaktor'
])
self.resultsTable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
[docs]
def setupAdditionalResultsTable(self):
"""
Sets up the additional results table.
"""
self.additionalResultsTable = QTableWidget()
self.additionalResultsTable.setColumnCount(3)
self.additionalResultsTable.setHorizontalHeaderLabels(['Ergebnis', 'Wert', 'Einheit'])
self.additionalResultsTable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
[docs]
def adjustTableSize(self, table):
"""
Adjusts the size of the table to fit its contents.
:param table: The table to adjust
:type table: QTableWidget
"""
header_height = table.horizontalHeader().height()
rows_height = sum([table.rowHeight(i) for i in range(table.rowCount())])
table.setFixedHeight(header_height + rows_height)
[docs]
def updateResults(self, energy_system):
"""
Updates the results in the ResultsTab.
:param energy_system: The energy system instance containing results
:type energy_system: EnergySystem
"""
self.energy_system = energy_system
self.showResultsInTable()
self.showAdditionalResultsTable()
self.plotResults()
self.updatePieChart()
[docs]
def showResultsInTable(self):
"""
Displays the results in the results table, including calculated operational metrics.
Resets the table rows before populating to avoid leftover rows from previous calculations.
"""
results = self.energy_system.results
# Reset the table to the correct number of rows
self.resultsTable.setRowCount(0)
self.resultsTable.setRowCount(len(results['techs']))
# Iterate over each technology and calculate operational metrics
for i, (tech, wärmemenge, wgk, anteil, spec_emission, primary_energy, wärmeleistung) in enumerate(
zip(results['techs'], results['Wärmemengen'], results['WGK'],
results['Anteile'], results['specific_emissions_L'], results['primärenergie_L'], results['Wärmeleistung_L'])):
# Ensure wärmeleistung is treated as a NumPy array for consistency
if not isinstance(wärmeleistung, (list, np.ndarray)):
wärmeleistung = [wärmeleistung] # Convert scalar to list for single value cases
wärmeleistung = np.array(wärmeleistung)
# Calculate 'Anzahl Betriebsstunden' (sum of hours with non-zero output)
betriebsstunden = np.count_nonzero(wärmeleistung) # Counts all non-zero hours
# Calculate 'Anzahl Starts' (counts transitions from 0 to > 0)
starts = np.sum((wärmeleistung[:-1] == 0) & (wärmeleistung[1:] > 0))
# Calculate 'Betriebsstunden/Start' (average hours per start)
betriebsstunden_pro_start = betriebsstunden / starts if starts > 0 else 0
# Populate the table with calculated values
self.resultsTable.setItem(i, 0, QTableWidgetItem(tech))
self.resultsTable.setItem(i, 1, QTableWidgetItem(f"{np.sum(wärmemenge):.2f}"))
self.resultsTable.setItem(i, 2, QTableWidgetItem(f"{betriebsstunden}"))
self.resultsTable.setItem(i, 3, QTableWidgetItem(f"{starts}"))
self.resultsTable.setItem(i, 4, QTableWidgetItem(f"{betriebsstunden_pro_start:.2f}"))
self.resultsTable.setItem(i, 5, QTableWidgetItem(f"{wgk:.2f}"))
self.resultsTable.setItem(i, 6, QTableWidgetItem(f"{anteil * 100:.2f}"))
self.resultsTable.setItem(i, 7, QTableWidgetItem(f"{spec_emission:.4f}"))
self.resultsTable.setItem(i, 8, QTableWidgetItem(f"{primary_energy / np.sum(wärmemenge):.4f}"))
self.resultsTable.resizeColumnsToContents()
self.adjustTableSize(self.resultsTable)
[docs]
def showAdditionalResultsTable(self):
"""
Displays the additional results in the additional results table.
"""
# The following calculations really need to be done in the energy system class
# Big Problem that needs to be addressed, as BEW-subsidies are not included in the current results if available
self.waerme_ges_kW, self.strom_wp_kW = np.sum(self.energy_system.results["waerme_ges_kW"]), np.sum(self.energy_system.results["strom_wp_kW"])
if 'Summe Infrastruktur' in self.parent.costTab.data.index:
self.WGK_Infra = self.parent.costTab.data.at['Summe Infrastruktur', 'Annuität'] / self.energy_system.results['Jahreswärmebedarf']
if self.energy_system.economic_parameters["subsidy_eligibility"] == "Ja":
self.WGK_Infra = (self.parent.costTab.data.at['Summe Infrastruktur', 'Annuität'] * 0.6) / self.energy_system.results['Jahreswärmebedarf']
else:
self.WGK_Infra = 0 # Fallback-Wert
self.wgk_heat_pump_electricity = ((self.strom_wp_kW/1000) * self.parent.economic_parameters["electricity_price"]) / ((self.strom_wp_kW+self.waerme_ges_kW)/1000)
self.WGK_Gesamt = self.energy_system.results['WGK_Gesamt'] + self.WGK_Infra + self.wgk_heat_pump_electricity
data = [
("Jahreswärmebedarf", round(self.energy_system.results['Jahreswärmebedarf'], 1), "MWh"),
("Stromerzeugung", round(self.energy_system.results['Strommenge'], 2), "MWh"),
("Strombedarf", round(self.energy_system.results['Strombedarf'], 2), "MWh"),
("Wärmegestehungskosten Erzeugeranlagen", round(self.energy_system.results['WGK_Gesamt'], 2), "€/MWh"),
("Wärmegestehungskosten Netzinfrastruktur", round(self.WGK_Infra, 2), "€/MWh"),
("Wärmegestehungskosten dezentrale Wärmepumpen", round(self.wgk_heat_pump_electricity, 2), "€/MWh"),
("Wärmegestehungskosten Gesamt", round(self.WGK_Gesamt, 2), "€/MWh"),
("spez. CO2-Emissionen Wärme", round(self.energy_system.results["specific_emissions_Gesamt"], 4), "t_CO2/MWh_th"),
("CO2-Emissionen Wärme", round(self.energy_system.results["specific_emissions_Gesamt"]*self.energy_system.results['Jahreswärmebedarf'], 2), "t_CO2"),
("Primärenergiefaktor", round(self.energy_system.results["primärenergiefaktor_Gesamt"], 4), "-")
]
self.additionalResultsTable.setRowCount(len(data))
for i, (description, value, unit) in enumerate(data):
self.additionalResultsTable.setItem(i, 0, QTableWidgetItem(description))
self.additionalResultsTable.setItem(i, 1, QTableWidgetItem(str(value)))
self.additionalResultsTable.setItem(i, 2, QTableWidgetItem(unit))
self.additionalResultsTable.resizeColumnsToContents()
self.adjustTableSize(self.additionalResultsTable)
[docs]
def plotResults(self):
"""
Plots the results in the diagrams.
"""
extracted_data, initial_vars = self.energy_system.getInitialPlotData()
# ComboBox aktualisieren
model = self.variableComboBox.model()
combo_items = [model.item(i).text() for i in range(model.rowCount())]
if set(extracted_data.keys()) != set(combo_items):
self.variableComboBox.clear()
self.variableComboBox.addItems(extracted_data.keys())
self.variableComboBox.addItem("Last_L")
for var in initial_vars:
self.variableComboBox.setItemChecked(var, True)
# Diagramm zeichnen
self.selected_variables = self.variableComboBox.checkedItems()
self.stackPlotFigure.clear()
self.energy_system.plot_stack_plot(
figure=self.stackPlotFigure,
selected_vars=self.selected_variables,
second_y_axis=self.secondYAxisCheckBox.isChecked()
)
self.stackPlotCanvas.draw()
[docs]
def updateSelectedVariables(self):
"""
Updates the selected variables and re-plots the diagram.
"""
self.selected_variables = self.variableComboBox.checkedItems()
self.stackPlotFigure.clear()
self.energy_system.plot_stack_plot(
figure=self.stackPlotFigure,
selected_vars=self.selected_variables,
second_y_axis=self.secondYAxisCheckBox.isChecked()
)
self.stackPlotCanvas.draw()
[docs]
def updatePieChart(self):
"""
Updates the pie chart with results from the EnergySystem.
"""
self.pieChartFigure.clear()
self.energy_system.plot_pie_chart(self.pieChartFigure)
self.pieChartCanvas.draw()
if __name__ == "__main__":
app = QApplication(sys.argv)
data_manager = None # Sie müssen hier ein geeignetes Datenmanager-Objekt übergeben
main = ResultsTab(data_manager)
main.show()
sys.exit(app.exec())