From 7bd19a219c6e4c1fc209c60a8ecdf0007d326d40 Mon Sep 17 00:00:00 2001 From: Ertan Coskun Date: Mon, 29 Jun 2026 04:18:20 +0200 Subject: [PATCH 1/4] Added robust model A robust model can now be created after starting the application on the menubar under the "compare action". Also added a new wizard to import two experiments. --- extrap/gui/MainWidget.py | 20 ++++ extrap/gui/components/robust_wizard.py | 137 +++++++++++++++++++++++++ extrap/modelers/robust.py | 108 +++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 extrap/gui/components/robust_wizard.py create mode 100644 extrap/modelers/robust.py diff --git a/extrap/gui/MainWidget.py b/extrap/gui/MainWidget.py index f44f779..1b7a346 100644 --- a/extrap/gui/MainWidget.py +++ b/extrap/gui/MainWidget.py @@ -42,6 +42,7 @@ from extrap.gui.SelectorWidget import SelectorWidget from extrap.gui.StrongScalingConversionDialog import StrongScalingConversionDialog 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 +50,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 @@ -251,6 +253,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 +280,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) @@ -428,6 +437,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/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/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 From ed77371bbb49d7229b7bfa82ca47ee998a3e2ae7 Mon Sep 17 00:00:00 2001 From: Ertan Coskun Date: Mon, 29 Jun 2026 04:20:56 +0200 Subject: [PATCH 2/4] measure arg available on mult modeler Personally I was confused and maybe wrong with this decission. But this change make more sense in my perspectiv. When using the ModelGenerator with a set use_measure, then should this decision also propagate to the modeler. --- extrap/modelers/model_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 0b79c42abbe57a0f15df8f6845509a074b887e78 Mon Sep 17 00:00:00 2001 From: Ertan Coskun <92693115+DotchPlayer@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:09:04 +0200 Subject: [PATCH 3/4] Modified Allowed DockAreas Set that every already existing DockWidget can be placed everywhere except at the top. --- extrap/gui/MainWidget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extrap/gui/MainWidget.py b/extrap/gui/MainWidget.py index 1b7a346..5ace3fb 100644 --- a/extrap/gui/MainWidget.py +++ b/extrap/gui/MainWidget.py @@ -117,6 +117,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 @@ -137,6 +138,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") @@ -144,6 +146,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) @@ -152,12 +155,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") @@ -165,6 +170,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") @@ -172,6 +178,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 From 69d510ac32ce396c505255a5333c9ae8d229252d Mon Sep 17 00:00:00 2001 From: Ertan Coskun <92693115+DotchPlayer@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:00:03 +0200 Subject: [PATCH 4/4] Added tabs for experiments When importing an experiment into the application a seperate tab will be created with the said experiment stored. With this switching between experiment is now established, but with some restriction. Because of the code structure the easiest implementation was an signal taskbar. Which when switching between tabs only saves the said experiment and some needed settings and extra information. By switching all dockwidget are set to the experiment which was selected to load. That brings the restriction that it is not possible two view multiple experiments concurrently. But it was possible to save the selected models inside of an experiment between switching. This can also been done when experiment are using different tree model filters like All or Flat. --- extrap/gui/MainWidget.py | 35 +++++++++++ extrap/gui/TabBar.py | 126 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 extrap/gui/TabBar.py diff --git a/extrap/gui/MainWidget.py b/extrap/gui/MainWidget.py index 5ace3fb..73d01f4 100644 --- a/extrap/gui/MainWidget.py +++ b/extrap/gui/MainWidget.py @@ -41,6 +41,7 @@ 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 @@ -125,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") @@ -354,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) @@ -367,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: 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) +