diff --git a/extrap/gui/MainWidget.py b/extrap/gui/MainWidget.py index f44f779..73d01f4 100644 --- a/extrap/gui/MainWidget.py +++ b/extrap/gui/MainWidget.py @@ -41,7 +41,9 @@ from extrap.gui.RankingWidget import RankingWidget from extrap.gui.SelectorWidget import SelectorWidget from extrap.gui.StrongScalingConversionDialog import StrongScalingConversionDialog +from extrap.gui.TabBar import TabBar from extrap.gui.comparison.comparison_wizard import ComparisonWizard +from extrap.gui.components.robust_wizard import RobustModelWizard from extrap.gui.components import file_dialog from extrap.gui.components.ProgressWindow import ProgressWindow from extrap.gui.components.about_dialog import AboutDialog @@ -49,6 +51,7 @@ from extrap.gui.components.model_color_map import ModelColorMap from extrap.gui.components.plot_formatting_options import PlotFormattingOptions, PlotFormattingDialog from extrap.modelers.model_generator import ModelGenerator +from extrap.modelers.robust import RobustModel from extrap.util.deprecation import deprecated from extrap.util.dynamic_options import DynamicOptions from extrap.util.event import Event @@ -115,6 +118,7 @@ def _init_ui(self): dock.setObjectName("EP_selection") self.selector_widget = SelectorWidget(self, dock) dock.setWidget(self.selector_widget) + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock) # middle: Graph @@ -122,6 +126,20 @@ def _init_ui(self): self.data_display = DataDisplayManager(self, self) central_widget = self.data_display + # upper: Tab bar for multiple experiments + self.dock_tabs = QDockWidget(self) + self.dock_tabs.setObjectName("EP_Tabs") + self.tab_widget = TabBar(self,parent=dock) + self.tab_widget.setMinimumHeight(30) + self.tab_widget.setTabsClosable(False) + self.tab_widget.tabCloseRequested.connect(self.tab_widget.close_tab) + self.tab_widget.currentChanged.connect(self.tab_widget.on_tab_changed) + self.dock_tabs.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) + self.dock_tabs.setWidget(self.tab_widget) + self.addDockWidget(Qt.DockWidgetArea.TopDockWidgetArea, self.dock_tabs, Qt.Orientation.Horizontal) + self.dock_tabs.hide() + + # Right side: Model configurator dock = QDockWidget("Modeler", self) dock.setObjectName("EP_modeler") @@ -135,6 +153,7 @@ def _init_ui(self): self.measurementWizard_widget = MeasurementWizardWidget(self, dock) dock.setWidget(self.measurementWizard_widget) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock) + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) dock2 = QDockWidget("Aggregation and Analysis", self) dock2.setObjectName("EP_agg_ana") @@ -142,6 +161,7 @@ def _init_ui(self): dock2.setWidget(self.postprocessing_widget) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock2) self.tabifyDockWidget(dock2, dock) + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) # bottom widget dock = QDockWidget("Color Info", self) @@ -150,12 +170,14 @@ def _init_ui(self): self.min_max_value_updated_event += self.color_widget.update_min_max dock.setWidget(self.color_widget) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock) + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) dock = QDockWidget("Graph Limits", self) dock.setObjectName("EP_graph_limits") self.graph_limits_widget = GraphLimitsWidget(self, self.data_display) dock.setWidget(self.graph_limits_widget) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock, Qt.Orientation.Horizontal) + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) dock2 = QDockWidget("Log", self) dock2.setObjectName("EP_log") @@ -163,6 +185,7 @@ def _init_ui(self): dock2.setWidget(self.log_widget) self.tabifyDockWidget(dock, dock2) dock2.hide() + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) dock2 = QDockWidget("Ranking", self) dock2.setObjectName("EP_ranking") @@ -170,6 +193,7 @@ def _init_ui(self): dock2.setWidget(self.ranking_widget) self.tabifyDockWidget(dock, dock2) dock2.hide() + dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas ^ Qt.DockWidgetArea.TopDockWidgetArea) # Menu creation # File menu @@ -251,6 +275,11 @@ def _init_ui(self): compare_action.setEnabled(False) self.compare_action = compare_action + # robust model creation menu + create_robust_action = QAction('Create robust model', self) + create_robust_action.setStatusTip('Creates a robust model with two experiments') + create_robust_action.triggered.connect(self.create_robust_model_wizard) + # Filter menu # filter_callpath_action = QAction('Filter Callpaths', self) # filter_callpath_action.setShortcut('Ctrl+F') @@ -273,6 +302,8 @@ def _init_ui(self): file_menu.addAction(save_experiment_action) file_menu.addAction(compare_action) file_menu.addSeparator() + file_menu.addAction(create_robust_action) + file_menu.addSeparator() file_menu.addAction(screenshot_action) file_menu.addSeparator() file_menu.addAction(exit_action) @@ -338,6 +369,24 @@ def set_experiment(self, experiment, file_name="", *, compared=False): raise ValueError("Experiment cannot be none.") self.experiment_change = True self._experiment = experiment + # Add tab if this is a new experiment + + tab_name = Path(file_name).name if file_name else "Experiment" + is_new_experiment = not any(e == experiment for e, *_ in self.tab_widget.experiments) + if is_new_experiment: + self.tab_widget._save_tab_state(self.tab_widget._active_tab_index) + self.tab_widget.experiments.append((experiment, file_name)) + new_tab_index = self.tab_widget.addTab(tab_name) + self._active_tab_index = new_tab_index + self.tab_widget.blockSignals(True) + self.tab_widget.setCurrentIndex(new_tab_index) + self.tab_widget.blockSignals(False) + self.tab_widget.setTabsClosable(True) + + if len(self.tab_widget.experiments) == 1: + self.tab_widget.setTabsClosable(False) + self.dock_tabs.show() + if file_name is not None: self._set_opened_file_name(file_name, compared=compared) self.save_experiment_action.setEnabled(True) @@ -351,6 +400,8 @@ def set_experiment(self, experiment, file_name="", *, compared=False): self.experiment_change = False self.updateMinMaxValue() self.update() + if is_new_experiment: + self.tab_widget._restore_tab_state(self._active_tab_index) def on_selection_changed(self): if not self.experiment_change: @@ -428,6 +479,17 @@ def on_accept(): def getFontSize(self): return self.plot_formatting_options.font_size + def create_robust_model_wizard(self): + rw = RobustModelWizard() + + def on_accept(): + robust_experiment = RobustModel.createRobustModel( + experiment_BB=rw.exp_swc,experiment_TIME=rw.exp_time,use_measure=rw.use_measure) + self.set_experiment(robust_experiment) + + rw.accepted.connect(on_accept) + rw.open() + def screenshot(self, _checked=False, target=None, name_addition=""): """ This function creates a screenshot of this or the target widget diff --git a/extrap/gui/TabBar.py b/extrap/gui/TabBar.py new file mode 100644 index 0000000..692d8e6 --- /dev/null +++ b/extrap/gui/TabBar.py @@ -0,0 +1,126 @@ +# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p) +# +# Copyright (c) 2020-2025, Technical University of Darmstadt, Germany +# +# This software may be modified and distributed under the terms of a BSD-style license. +# See the LICENSE file in the base directory for details. + + +from pathlib import Path + +from PySide6.QtCore import * # @UnusedWildImport +from PySide6.QtGui import * # @UnusedWildImport +from PySide6.QtWidgets import * # @UnusedWildImport + +import extrap +from extrap.util.event import Event +from PySide6.QtCore import * # @UnusedWildImport +from PySide6.QtGui import * # @UnusedWildImport +from PySide6.QtWidgets import * # @UnusedWildImport + +class TabBar(QTabBar): + # Store experiments + + def __init__(self, main_widget: MainWidget, parent): + super().__init__(parent) + self.main_widget = main_widget + self.experiments = [] + self._active_tab_index = 0 + + def close_tab(self, tab_index: int): + if self.count() == 1: + return + self.blockSignals(True) + self.removeTab(tab_index) + self.blockSignals(False) + self.experiments.pop(tab_index) + if self._active_tab_index > tab_index: + self._active_tab_index -= 1 + elif self._active_tab_index == tab_index: + self._active_tab_index = self.currentIndex() + if self.count() > 0: + index = self.currentIndex() + if 0 <= index < len(self.experiments): + self._active_tab_index = index + experiment, file_name = self.experiments[index][:2] + self.main_widget.set_experiment(experiment, file_name) + self._restore_tab_state(index) + + def _save_tab_state(self, tab_index: int): + """Save the metric index and selected callpath rows for tab_index so they can be restored later.""" + if 0 <= tab_index < len(self.experiments): + exp, file_name, *_ = self.experiments[tab_index] + metric_index = self.main_widget.selector_widget.metric_selector.currentIndex() + tree_filter = self.main_widget.selector_widget.tree_display_select.currentIndex() + + all_selected_nodes = [idx for idx in self.main_widget.selector_widget.tree_view.selectedIndexes() if idx.column() == 0] + traces = [] + + for selected_node in all_selected_nodes: + #selected_node contains with parent, row , column but only the first are the important + #get current row and check for parent + trace_list = [] + trace_list.append(selected_node.row()) + parent = selected_node.parent() + while parent.isValid(): + trace_list.insert(0, parent.row()) + parent = parent.parent() + traces.append(trace_list) + + self.experiments[tab_index] = (exp, file_name, metric_index,traces,tree_filter) + + def _restore_tab_state(self, index: int): + """Restore metric selection and callpath selection for the given tab index.""" + entry = self.experiments[index] + if len(entry) < 4: + # No saved state yet — auto-expand and select the first visible node + self.main_widget.selector_widget.tree_view.expandAll() + first = self.main_widget.selector_widget.tree_model.index(0, 0) + if first.isValid(): + self.main_widget.selector_widget.tree_view.setCurrentIndex(first) + return + _, _, metric_index,traces,tree_filter= entry + + #self.selector_widget.tree_view = tree_view + + if metric_index >= 0: + self.main_widget.selector_widget.metric_selector.setCurrentIndex(metric_index) + + sel_model = self.main_widget.selector_widget.tree_view.selectionModel() + sel_model.clearSelection() + + self.main_widget.selector_widget.tree_display_select.setCurrentIndex(tree_filter) + + for trace in traces: + parent = None + for i in range(0,len(trace)): + if i == 0: + index = self.main_widget.selector_widget.tree_model.index(trace[i], 0) + if index.isValid(): + parent = index + if i == len(trace) - 1: + sel_model.select(index, sel_model.SelectionFlag.Select | sel_model.SelectionFlag.Rows) + else: + self.main_widget.selector_widget.tree_view.expand(index) + continue + + if i == len(trace) - 1: + index = self.main_widget.selector_widget.tree_model.index(trace[i], 0,parent) + if index.isValid(): + sel_model.select(index, sel_model.SelectionFlag.Select | sel_model.SelectionFlag.Rows) + continue + + index = self.main_widget.selector_widget.tree_model.index(trace[i], 0, parent) + if index.isValid(): + self.main_widget.selector_widget.tree_view.expand(index) + parent = index + + + def on_tab_changed(self, index: int): + if 0 <= index < len(self.experiments): + self._save_tab_state(self._active_tab_index) + self._active_tab_index = index + experiment, file_name = self.experiments[index][:2] + self.main_widget.set_experiment(experiment, file_name) + self._restore_tab_state(index) + diff --git a/extrap/gui/components/robust_wizard.py b/extrap/gui/components/robust_wizard.py new file mode 100644 index 0000000..bb23dec --- /dev/null +++ b/extrap/gui/components/robust_wizard.py @@ -0,0 +1,137 @@ +# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p) +# +# Copyright (c) 2021-2025, Technical University of Darmstadt, Germany +# +# This software may be modified and distributed under the terms of a BSD-style license. +# See the LICENSE file in the base directory for details. + +import json +import math +import warnings +from asyncio import Event +from functools import partial +from itertools import chain +from json import JSONDecodeError +from pathlib import Path +from typing import Optional, Type, cast + +from PySide6.QtCore import Qt, Slot +from PySide6.QtWidgets import (QCommandLinkButton, QFileDialog, QFormLayout, + QLabel, QLineEdit, QSizePolicy, QSpacerItem, + QWizard, QWizardPage, QComboBox, QGridLayout, QButtonGroup, QHBoxLayout, QWidget, + QRadioButton, QListWidget, QGroupBox, QDoubleSpinBox, QPushButton, QSpinBox, + QListWidgetItem, QAbstractItemView, QVBoxLayout) + +from extrap.fileio.experiment_io import ExperimentReader +from extrap.fileio.file_reader import FileReader, all_readers +from extrap.gui.components import file_dialog +from extrap.gui.components.dynamic_options import DynamicOptionsWidget +from extrap.gui.components.wizard_pages import ProgressPage, ScrollAreaPage +from extrap.util.dynamic_options import DynamicOptions +from extrap.modelers.robust import RobustModel +from extrap.entities.measurement import Measure + +class RobustModelWizard(QWizard): + file_reader: Type[FileReader] + file_name: str + + def __init__(self, experiment1=None, experiment2=None, use_measure=Measure.MEAN): + super().__init__() + + self.setWindowTitle("Create Robust Model With Time Experiment") + self.setWizardStyle(QWizard.WizardStyle.ModernStyle) + self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) + + self.exp_swc = experiment1 + self.exp_time = experiment2 + self.use_measure = use_measure + + self.is_cancelled = Event() + + self.file_reader = None + self.rejected.connect(self.on_reject) + + if not experiment1: + self.addPage(FileSelectionPage(self, is_swc=True)) + self.addPage(FileReaderOptionsPage(self)) + self.file_loading_page_id = self.addPage(FileLoadingPage(self, is_swc=True)) + if not experiment2: + self.addPage(FileSelectionPage(self, is_swc=False)) + self.addPage(FileReaderOptionsPage(self)) + self.file_loading_page_id = self.addPage(FileLoadingPage(self, is_swc=False)) + + def on_reject(self): + self.is_cancelled.set() + + def back(self) -> None: + self.restart() + + + + +class FileSelectionPage(ScrollAreaPage): + def __init__(self, parent,is_swc): + super().__init__(parent) + if is_swc: + self.setTitle('Select SWC experiment format') + else: + self.setTitle('Select time experiment format') + layout = self.scroll_layout + for reader in chain([ExperimentReader], all_readers.values()): + def _(reader): + btn = QCommandLinkButton(reader.GUI_ACTION.replace('&', '')) + # btn.setDescription(reader.DESCRIPTION) + btn.clicked.connect( + lambda: file_dialog.show(self, partial(self.open_file, reader), reader.DESCRIPTION, + filter=reader.FILTER, + file_mode=QFileDialog.FileMode.Directory if reader.LOADS_FROM_DIRECTORY else None)) + layout.addWidget(btn) + + _(reader) + layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)) + self.scroll_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)) + + def open_file(self, reader, name): + wizard: RobustModelWizard = cast(RobustModelWizard, self.wizard()) + wizard.file_name = name + wizard.file_reader = reader() + wizard.next() + + def nextId(self) -> int: + wizard: RobustModelWizard = cast(RobustModelWizard, self.wizard()) + if not isinstance(wizard.file_reader, DynamicOptions): + return wizard.file_loading_page_id + else: + return super().nextId() + + def isComplete(self) -> bool: + return False + +class FileReaderOptionsPage(QWizardPage): + def __init__(self, parent: RobustModelWizard): + super().__init__(parent) + self.setTitle('File import options') + layout = QVBoxLayout(self) + self.setLayout(layout) + self.dynamic_options_widget = DynamicOptionsWidget(self, None) + layout.addWidget(self.dynamic_options_widget) + + def initializePage(self) -> None: + wizard: RobustModelWizard = cast(RobustModelWizard, self.wizard()) + self.dynamic_options_widget.update_object_with_options(wizard.file_reader) + +class FileLoadingPage(ProgressPage): + def __init__(self, parent,is_swc): + super().__init__(parent) + self.setTitle('Loading experiment') + self.is_swc = is_swc + + def do_process(self, pbar): + wizard: RobustModelWizard = cast(RobustModelWizard, self.wizard()) + if self.is_swc: + wizard.exp_swc = wizard.file_reader.read_experiment(wizard.file_name, pbar) + else: + wizard.exp_time = wizard.file_reader.read_experiment(wizard.file_name, pbar) + + + diff --git a/extrap/modelers/model_generator.py b/extrap/modelers/model_generator.py index 6e7c40e..f108a93 100644 --- a/extrap/modelers/model_generator.py +++ b/extrap/modelers/model_generator.py @@ -86,7 +86,7 @@ def _choose_modeler(self, modeler: Union[AbstractModeler, str], use_measure) -> raise ValueError( f'A modeler with name "{modeler}" does not exist. For multi-parameter experiments only the ' f'following modelers are available "{", ".join(multi_parameter.all_modelers.keys())}".') from e - result_modeler.use_measure = use_measure + result_modeler.use_measure = use_measure elif modeler is NotImplemented: result_modeler = NotImplemented diff --git a/extrap/modelers/robust.py b/extrap/modelers/robust.py new file mode 100644 index 0000000..fa478f3 --- /dev/null +++ b/extrap/modelers/robust.py @@ -0,0 +1,108 @@ +import signal +import sys +from enum import Enum +from functools import partial +from numbers import Number +from pathlib import Path +from typing import Optional, Sequence, Tuple, Type + +import numpy +import random +import copy +from PySide6.QtCore import * # @UnusedWildImport +from PySide6.QtGui import * # @UnusedWildImport +from PySide6.QtWidgets import * # @UnusedWildImport + +import extrap + +from extrap.entities.callpath import Callpath +from extrap.entities.hypotheses import SingleParameterHypothesis, ConstantHypothesis, MultiParameterHypothesis +from extrap.entities.measurement import Measurement +from extrap.entities.measurement import Measure +from extrap.entities.metric import Metric +from extrap.util.progress_bar import DUMMY_PROGRESS +from extrap.entities.experiment import Experiment + +from extrap.modelers.model_generator import ModelGenerator + +class RobustModel(): + + @staticmethod + def checkingEqualCallPath(exp_bb, exp_time): + if len(exp_bb.callpaths) != len(exp_time.callpaths): + return False + + for path in exp_bb.callpaths: + if path not in exp_time.callpaths: + return False + + return True + + @staticmethod + def creating_robust_experiment(bb_experiment, experiment_TIME): + experiment_Robust = copy.deepcopy(bb_experiment) + experiment_Robust.modelers.clear() + for i in range(0, len(experiment_Robust.callpaths)): + if "MPI_" in str(experiment_Robust.callpaths[i]): + try: + measurement_values = experiment_TIME.measurements[ + (Callpath(str(experiment_TIME.callpaths[i])), Metric('bytes_sent'))] + experiment_Robust.measurements.update( + {(Callpath(str(experiment_TIME.callpaths[i])), Metric('bytes_sent')): measurement_values}) + except KeyError: + try: + measurement_values = experiment_TIME.measurements[ + (Callpath(str(experiment_TIME.callpaths[i])), Metric('bytes_received')) + ] + experiment_Robust.measurements.update( + {(Callpath(str(experiment_TIME.callpaths[i])),Metric('bytes_received')):measurement_values}) + except KeyError: + continue + + return experiment_Robust + + @staticmethod + def createRobustModel(experiment_BB,experiment_TIME,use_measure=Measure.MEAN): + is_same_callpath : bool = RobustModel.checkingEqualCallPath(exp_bb=experiment_BB,exp_time=experiment_TIME) + + if not is_same_callpath: + raise Exception("Not equal callpath") + + experiment_Robust = RobustModel.creating_robust_experiment(experiment_BB,experiment_TIME) + model_Robust = ModelGenerator(experiment=experiment_Robust, name="Robust", use_measure=use_measure) + model_Robust.model_all() + + RobustModel.manipulate_all_functions(model_Robust, experiment_TIME.measurements) + return experiment_Robust + + @staticmethod + def manipulate_all_functions(model_robust, time_measurements): + models = list(model_robust.models.items()) + for i in range(0, len(models)): + cur_model_tuple = models[i] # Creates a Tuple with (Callpath,Metric),Model + cur_time_measurement = time_measurements[cur_model_tuple[0]] + if isinstance(cur_model_tuple[1].hypothesis, ConstantHypothesis): + RobustModel.modeling_constant(cur_model_tuple[1], cur_time_measurement) + if isinstance(cur_model_tuple[1].hypothesis, SingleParameterHypothesis): + RobustModel.modeling_with_param(cur_model_tuple[1], cur_time_measurement) + if isinstance(cur_model_tuple[1].hypothesis, MultiParameterHypothesis): + RobustModel.modeling_with_param(cur_model_tuple[1], cur_time_measurement) + + + @staticmethod + def modeling_constant(model, measurements): + # use all available additional points for modeling the multi-parameter models + model.hypothesis.compute_coefficients(measurements) + + @staticmethod + def modeling_with_param(model, measurement): + model.hypothesis.compute_coefficients(measurement) + model.hypothesis.compute_cost(measurement) + values = numpy.fromiter( + Measurement.select_measure(measurement, model.hypothesis._use_measure), + float, + len(measurement) + ) + meanModel = numpy.mean(values) + constantCost = numpy.sum((values - meanModel) * (values - meanModel)) + model.hypothesis.compute_adjusted_rsquared(constantCost,measurement) \ No newline at end of file