Source code for districtheatingsim.gui.EnergySystemTab._07_results_tab

"""
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())