From e607feb6e67d877d346ee1ff67c2248acb4e8e26 Mon Sep 17 00:00:00 2001 From: AugustoMagalhaes <86219529+AugustoMagalhaes@users.noreply.github.com> Date: Mon, 30 Sep 2024 06:44:06 -0300 Subject: [PATCH] Group items in Experiment Panel's drop-down --- .../simulation/combobox_with_description.py | 44 ++++++++++--- src/ert/gui/simulation/experiment_panel.py | 62 +++++++++++++------ src/ert/run_models/base_run_model.py | 8 +++ src/ert/run_models/ensemble_experiment.py | 6 +- src/ert/run_models/evaluate_ensemble.py | 2 +- .../run_models/iterated_ensemble_smoother.py | 2 +- .../run_models/multiple_data_assimilation.py | 10 ++- src/ert/run_models/single_test_run.py | 8 ++- tests/ert/ui_tests/gui/test_main_window.py | 14 ++--- .../ert/ui_tests/gui/test_single_test_run.py | 3 + .../gui/simulation/test_run_dialog.py | 4 +- 11 files changed, 118 insertions(+), 45 deletions(-) diff --git a/src/ert/gui/simulation/combobox_with_description.py b/src/ert/gui/simulation/combobox_with_description.py index 3e95b9c7430..51784a1a42e 100644 --- a/src/ert/gui/simulation/combobox_with_description.py +++ b/src/ert/gui/simulation/combobox_with_description.py @@ -14,6 +14,7 @@ LABEL_ROLE = -3994 DESCRIPTION_ROLE = -4893 +GROUP_TITLE_ROLE = -4894 COLOR_HIGHLIGHT_LIGHT = QColor(230, 230, 230, 255) COLOR_HIGHLIGHT_DARK = QColor(60, 60, 60, 255) @@ -26,19 +27,34 @@ def __init__( description: str, enabled: bool = True, parent: Optional[QWidget] = None, + group: Optional[str] = None, ) -> None: super().__init__(parent) layout = QVBoxLayout() - layout.setSpacing(2) + layout.setSpacing(5) self.setStyleSheet("background: rgba(0,0,0,1);") self.label = QLabel(label) color = "color: rgba(192,192,192,80);" if not enabled else ";" + pd_top = "0px" if group else "5px" + if group: + self.group = QLabel(group) + self.group.setStyleSheet( + f""" + {color} + padding-top: 5px; + padding-left: 2px; + background: rgba(0,0,0,0); + font-style: italic; + font-size: 14px; + """ + ) + layout.addWidget(self.group) self.label.setStyleSheet( f""" {color} - padding-top:5px; - padding-left: 5px; + padding-top:{pd_top}; + padding-left: 10px; background: rgba(0,0,0,0); font-weight: bold; font-size: 13px; @@ -49,7 +65,7 @@ def __init__( f""" {color} padding-bottom: 10px; - padding-left: 10px; + padding-left: 15px; background: rgba(0,0,0,0); font-style: italic; font-size: 12px; @@ -67,6 +83,7 @@ def paint(self, painter: Any, option: Any, index: Any) -> None: label = index.data(LABEL_ROLE) description = index.data(DESCRIPTION_ROLE) + group = index.data(GROUP_TITLE_ROLE) is_enabled = option.state & QStyle.State_Enabled # type: ignore @@ -79,7 +96,7 @@ def paint(self, painter: Any, option: Any, index: Any) -> None: color = COLOR_HIGHLIGHT_DARK painter.fillRect(option.rect, color) - widget = _ComboBoxItemWidget(label, description, is_enabled) + widget = _ComboBoxItemWidget(label, description, is_enabled, group=group) widget.setStyle(option.widget.style()) widget.resize(option.rect.size()) @@ -90,9 +107,11 @@ def paint(self, painter: Any, option: Any, index: Any) -> None: def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: label = index.data(LABEL_ROLE) description = index.data(DESCRIPTION_ROLE) + group = index.data(GROUP_TITLE_ROLE) + adjustment = QSize(0, 20) if group else QSize(0, 0) - widget = _ComboBoxItemWidget(label, description) - return widget.sizeHint() + widget = _ComboBoxItemWidget(label, description, group) + return widget.sizeHint() + adjustment class QComboBoxWithDescription(QComboBox): @@ -100,10 +119,19 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.setItemDelegate(_ComboBoxWithDescriptionDelegate(self)) - def addDescriptionItem(self, label: Optional[str], description: Any) -> None: + def addDescriptionItem( + self, label: Optional[str], description: Any, group: Optional[str] = None + ) -> None: super().addItem(label) model = self.model() assert model is not None index = model.index(self.count() - 1, 0) model.setData(index, label, LABEL_ROLE) model.setData(index, description, DESCRIPTION_ROLE) + model.setData(index, group, GROUP_TITLE_ROLE) + + def sizeHint(self) -> QSize: + original_size_hint = super().sizeHint() + new_width = int(original_size_hint.width() + 220) + new_height = int(super().sizeHint().height() * 1.5) + return QSize(new_width, new_height) diff --git a/src/ert/gui/simulation/experiment_panel.py b/src/ert/gui/simulation/experiment_panel.py index 32f1bd94963..3ad875eb821 100644 --- a/src/ert/gui/simulation/experiment_panel.py +++ b/src/ert/gui/simulation/experiment_panel.py @@ -17,7 +17,6 @@ QCheckBox, QFrame, QHBoxLayout, - QLabel, QMessageBox, QStackedWidget, QStyle, @@ -49,8 +48,7 @@ if TYPE_CHECKING: from ert.config import ErtConfig -EXPERIMENT_READY_TO_RUN_BUTTON_MESSAGE = "Run Experiment" -EXPERIMENT_IS_RUNNING_BUTTON_MESSAGE = "Experiment running..." +EXPERIMENT_IS_MANUAL_UPDATE_MESSAGE = "Execute Selected" def create_md_table(kv: Dict[str, str], output: str) -> str: @@ -89,9 +87,6 @@ def __init__( experiment_type_layout = QHBoxLayout() experiment_type_layout.addSpacing(10) - experiment_type_layout.addWidget( - QLabel("Experiment type:"), 0, Qt.AlignmentFlag.AlignVCenter - ) experiment_type_layout.addWidget( self._experiment_type_combo, 0, Qt.AlignmentFlag.AlignVCenter ) @@ -100,11 +95,34 @@ def __init__( self.run_button = QToolButton() self.run_button.setObjectName("run_experiment") - self.run_button.setText(EXPERIMENT_READY_TO_RUN_BUTTON_MESSAGE) self.run_button.setIcon(QIcon("img:play_circle.svg")) + self.run_button.setToolTip(EXPERIMENT_IS_MANUAL_UPDATE_MESSAGE) self.run_button.setIconSize(QSize(32, 32)) self.run_button.clicked.connect(self.run_experiment) - self.run_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.run_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) + self.run_button.setMinimumWidth(60) + self.run_button.setMinimumHeight(40) + self.run_button.setStyleSheet( + """ + QToolButton { + border-radius: 10px; + background-color: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #f0f0f0, + stop:1 #d9d9d9 + ); + border: 1px solid #bfbfbf; + padding: 5px; + } + QToolButton:hover { + background-color: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #d8d8d8, + stop:1 #c3c3c3 + ); + } + """ + ) experiment_type_layout.addWidget(self.run_button) experiment_type_layout.addStretch(1) @@ -138,26 +156,25 @@ def __init__( ) analysis_config = config.analysis_config self.addExperimentConfigPanel( - EnsembleSmootherPanel(analysis_config, run_path, notifier, ensemble_size), + MultipleDataAssimilationPanel( + analysis_config, run_path, notifier, ensemble_size + ), experiment_type_valid, ) self.addExperimentConfigPanel( - ManualUpdatePanel(ensemble_size, run_path, notifier, analysis_config), + EnsembleSmootherPanel(analysis_config, run_path, notifier, ensemble_size), experiment_type_valid, ) self.addExperimentConfigPanel( - MultipleDataAssimilationPanel( + IteratedEnsembleSmootherPanel( analysis_config, run_path, notifier, ensemble_size ), experiment_type_valid, ) self.addExperimentConfigPanel( - IteratedEnsembleSmootherPanel( - analysis_config, run_path, notifier, ensemble_size - ), + ManualUpdatePanel(ensemble_size, run_path, notifier, analysis_config), experiment_type_valid, ) - self.setLayout(layout) def addExperimentConfigPanel( @@ -168,7 +185,9 @@ def addExperimentConfigPanel( experiment_type = panel.get_experiment_type() self._experiment_widgets[experiment_type] = panel self._experiment_type_combo.addDescriptionItem( - experiment_type.name(), experiment_type.description() + experiment_type.name(), + experiment_type.description(), + experiment_type.group(), ) if not mode_enabled: @@ -294,12 +313,10 @@ def run_experiment(self) -> None: dialog.produce_clipboard_debug_info.connect(self.populate_clipboard_debug_info) self.run_button.setEnabled(False) - self.run_button.setText(EXPERIMENT_IS_RUNNING_BUTTON_MESSAGE) dialog.run_experiment() dialog.show() def exit_handler() -> None: - self.run_button.setText(EXPERIMENT_READY_TO_RUN_BUTTON_MESSAGE) self.run_button.setEnabled(True) self.toggleExperimentType() self._notifier.emitErtChange() @@ -316,9 +333,14 @@ def toggleExperimentType(self) -> None: def validationStatusChanged(self) -> None: widget = self._experiment_widgets[self.get_current_experiment_type()] + widgets = QApplication.topLevelWidgets() + is_run_dialog_open = False + for w in widgets: + if isinstance(w, RunDialog) and w.isVisible(): + is_run_dialog_open = True + break self.run_button.setEnabled( - self.run_button.text() == EXPERIMENT_READY_TO_RUN_BUTTON_MESSAGE - and widget.isConfigurationValid() + not is_run_dialog_open and widget.isConfigurationValid() ) def populate_clipboard_debug_info(self) -> None: diff --git a/src/ert/run_models/base_run_model.py b/src/ert/run_models/base_run_model.py index 2b1b2c9adf6..4289501c9a4 100644 --- a/src/ert/run_models/base_run_model.py +++ b/src/ert/run_models/base_run_model.py @@ -215,6 +215,14 @@ def name(cls) -> str: ... @abstractmethod def description(cls) -> str: ... + @classmethod + def group(cls) -> Optional[str]: + """Default value to prevent errors in children classes + since only EnsembleExperiment and EnsembleSmoother should + override it + """ + return None + def send_event(self, event: StatusEvents) -> None: self._status_queue.put(event) diff --git a/src/ert/run_models/ensemble_experiment.py b/src/ert/run_models/ensemble_experiment.py index b2fc1243912..25348477b8f 100644 --- a/src/ert/run_models/ensemble_experiment.py +++ b/src/ert/run_models/ensemble_experiment.py @@ -105,4 +105,8 @@ def name(cls) -> str: @classmethod def description(cls) -> str: - return "Sample parameters → evaluate (N realizations)" + return "Sample parameters → evaluate all realizations" + + @classmethod + def group(cls) -> Optional[str]: + return None diff --git a/src/ert/run_models/evaluate_ensemble.py b/src/ert/run_models/evaluate_ensemble.py index cb190035c2e..00737982bc1 100644 --- a/src/ert/run_models/evaluate_ensemble.py +++ b/src/ert/run_models/evaluate_ensemble.py @@ -86,4 +86,4 @@ def name(cls) -> str: @classmethod def description(cls) -> str: - return "Take existing ensemble parameters → evaluate" + return "Use existing parameters → evaluate" diff --git a/src/ert/run_models/iterated_ensemble_smoother.py b/src/ert/run_models/iterated_ensemble_smoother.py index c20d12ab2d0..c3a179f6830 100644 --- a/src/ert/run_models/iterated_ensemble_smoother.py +++ b/src/ert/run_models/iterated_ensemble_smoother.py @@ -225,4 +225,4 @@ def name(cls) -> str: @classmethod def description(cls) -> str: - return "Sample parameters → [Evaluate → update] for N iterations" + return "Sample parameters → [evaluate → update] several iterations" diff --git a/src/ert/run_models/multiple_data_assimilation.py b/src/ert/run_models/multiple_data_assimilation.py index 9fed4cbbd23..eb394b852b6 100644 --- a/src/ert/run_models/multiple_data_assimilation.py +++ b/src/ert/run_models/multiple_data_assimilation.py @@ -22,6 +22,8 @@ logger = logging.getLogger(__name__) +MULTIPLE_DATA_ASSIMILATION_GROUP = "Parameter update" + class MultipleDataAssimilation(UpdateRunModel): """ @@ -184,8 +186,12 @@ def parse_weights(weights: str) -> List[float]: @classmethod def name(cls) -> str: - return "Multiple Data Assimilation (ES MDA) - Recommended" + return "Multiple data assimilation" @classmethod def description(cls) -> str: - return "[Sample|restart] → [Evaluate → update] for each weight" + return "[Sample|restart] → [evaluate → update] for each weight" + + @classmethod + def group(cls) -> Optional[str]: + return MULTIPLE_DATA_ASSIMILATION_GROUP diff --git a/src/ert/run_models/single_test_run.py b/src/ert/run_models/single_test_run.py index a6da0e57276..e1d4337aa9c 100644 --- a/src/ert/run_models/single_test_run.py +++ b/src/ert/run_models/single_test_run.py @@ -12,6 +12,8 @@ from .base_run_model import StatusEvents +SINGLE_TEST_RUN_GROUP = "Forward model evaluation" + class SingleTestRun(EnsembleExperiment): """ @@ -49,4 +51,8 @@ def name(cls) -> str: @classmethod def description(cls) -> str: - return "Sample parameters → evaluate (one realization)" + return "Sample parameters → evaluate single realization" + + @classmethod + def group(cls) -> Optional[str]: + return SINGLE_TEST_RUN_GROUP diff --git a/tests/ert/ui_tests/gui/test_main_window.py b/tests/ert/ui_tests/gui/test_main_window.py index ef4c593eb51..11135873474 100644 --- a/tests/ert/ui_tests/gui/test_main_window.py +++ b/tests/ert/ui_tests/gui/test_main_window.py @@ -387,12 +387,9 @@ def test_that_es_mda_is_disabled_when_weights_are_invalid(qtbot): assert gui.windowTitle().startswith("ERT - poly.ert") combo_box = get_child(gui, QComboBox, name="experiment_type") - combo_box.setCurrentIndex(5) + combo_box.setCurrentIndex(3) - assert ( - combo_box.currentText() - == "Multiple Data Assimilation (ES MDA) - Recommended" - ) + assert combo_box.currentText() == "Multiple data assimilation" es_mda_panel = get_child(gui, QWidget, name="ES_MDA_panel") assert es_mda_panel @@ -754,12 +751,9 @@ def test_that_es_mda_restart_run_box_is_disabled_when_there_are_no_cases(qtbot): combo_box = get_child(gui, QComboBox, name="experiment_type") qtbot.mouseClick(combo_box, Qt.MouseButton.LeftButton) assert combo_box.count() == 7 - combo_box.setCurrentIndex(5) + combo_box.setCurrentIndex(3) - assert ( - combo_box.currentText() - == "Multiple Data Assimilation (ES MDA) - Recommended" - ) + assert combo_box.currentText() == "Multiple data assimilation" es_mda_panel = get_child(gui, QWidget, name="ES_MDA_panel") assert es_mda_panel diff --git a/tests/ert/ui_tests/gui/test_single_test_run.py b/tests/ert/ui_tests/gui/test_single_test_run.py index b13fa7a86ff..b74f71c2e41 100644 --- a/tests/ert/ui_tests/gui/test_single_test_run.py +++ b/tests/ert/ui_tests/gui/test_single_test_run.py @@ -27,6 +27,9 @@ def test_single_test_run_after_ensemble_experiment( with contextlib.suppress(FileNotFoundError): shutil.rmtree("poly_out") + simulation_mode_combo = experiment_panel.findChild(QComboBox) + simulation_mode_combo.setCurrentText("Single realization test-run") + run_experiment = get_child(experiment_panel, QWidget, name="run_experiment") qtbot.mouseClick(run_experiment, Qt.LeftButton) # The Run dialog opens, wait until done appears, then click done diff --git a/tests/ert/unit_tests/gui/simulation/test_run_dialog.py b/tests/ert/unit_tests/gui/simulation/test_run_dialog.py index 09a4ab08ae9..2542c85637e 100644 --- a/tests/ert/unit_tests/gui/simulation/test_run_dialog.py +++ b/tests/ert/unit_tests/gui/simulation/test_run_dialog.py @@ -489,12 +489,14 @@ def handle_error_dialog(run_dialog): assert "I failed :(" in text qtbot.mouseClick(error_dialog.box.buttons()[0], Qt.LeftButton) + simulation_mode_combo = gui.findChild(QComboBox) + simulation_mode_combo.setCurrentText("Single realization test-run") qtbot.mouseClick(run_experiment, Qt.LeftButton) - run_dialog = wait_for_child(gui, qtbot, RunDialog) QTimer.singleShot(100, lambda: handle_error_dialog(run_dialog)) qtbot.waitUntil(run_dialog.done_button.isVisible, timeout=200000) + run_dialog.close() @pytest.mark.integration_test