From 16eacf1322824b313e69482ba237c5b942fb8dae Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 20 Jun 2024 09:31:06 +0200 Subject: [PATCH] Create unique experiment names for GUI --- .../simulation/ensemble_experiment_panel.py | 35 ++++++++++++---- .../gui/simulation/ensemble_smoother_panel.py | 40 ++++++++++++------ .../gui/simulation/experiment_config_panel.py | 12 +++--- src/ert/gui/simulation/experiment_panel.py | 14 +++++-- .../iterated_ensemble_smoother_panel.py | 42 +++++++++++++------ .../multiple_data_assimilation_panel.py | 42 ++++++++++++++----- src/ert/storage/local_storage.py | 31 ++++++++++++++ .../gui/simulation/test_run_path_dialog.py | 6 +-- .../unit_tests/storage/test_local_storage.py | 34 ++++++++++++++- 9 files changed, 199 insertions(+), 57 deletions(-) diff --git a/src/ert/gui/simulation/ensemble_experiment_panel.py b/src/ert/gui/simulation/ensemble_experiment_panel.py index 1b775733e4e..568cd1f3fb5 100644 --- a/src/ert/gui/simulation/ensemble_experiment_panel.py +++ b/src/ert/gui/simulation/ensemble_experiment_panel.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from qtpy import QtCore +from qtpy.QtCore import Slot from qtpy.QtWidgets import QFormLayout, QLabel from ert.gui.ertnotifier import ErtNotifier @@ -35,14 +36,18 @@ def __init__(self, ensemble_size: int, run_path: str, notifier: ErtNotifier): lab.setAlignment(QtCore.Qt.AlignLeft) layout.addRow(lab) - self._name_field = StringBox( - TextModel(""), placeholder_text="ensemble-experiment" + self._experiment_name_field = StringBox( + TextModel(""), + placeholder_text=self.notifier.storage.get_unique_experiment_name( + ENSEMBLE_EXPERIMENT_MODE + ), ) - self._name_field.setMinimumWidth(250) - layout.addRow("Experiment name:", self._name_field) - self._name_field.setValidator( + self._experiment_name_field.setMinimumWidth(250) + layout.addRow("Experiment name:", self._experiment_name_field) + self._experiment_name_field.setValidator( NotInStorage(self.notifier.storage, "experiments") ) + self._ensemble_name_field = StringBox( TextModel(""), placeholder_text="ensemble" ) @@ -73,21 +78,33 @@ def __init__(self, ensemble_size: int, run_path: str, notifier: ErtNotifier): self._active_realizations_field.getValidationSupport().validationChanged.connect( # noqa self.simulationConfigurationChanged ) - self._name_field.getValidationSupport().validationChanged.connect( # noqa + self._experiment_name_field.getValidationSupport().validationChanged.connect( # noqa self.simulationConfigurationChanged ) self._ensemble_name_field.getValidationSupport().validationChanged.connect( # noqa self.simulationConfigurationChanged ) + self.notifier.ertChanged.connect(self._update_experiment_name_placeholder) + + @Slot(ExperimentConfigPanel) + def experimentTypeChanged(self, w: ExperimentConfigPanel) -> None: + if isinstance(w, EnsembleExperimentPanel): + self._update_experiment_name_placeholder() + + def _update_experiment_name_placeholder(self) -> None: + self._experiment_name_field.setPlaceholderText( + self.notifier.storage.get_unique_experiment_name(ENSEMBLE_EXPERIMENT_MODE) + ) + def isConfigurationValid(self) -> bool: self.blockSignals(True) - self._name_field.validateString() + self._experiment_name_field.validateString() self._ensemble_name_field.validateString() self.blockSignals(False) return ( self._active_realizations_field.isValid() - and self._name_field.isValid() + and self._experiment_name_field.isValid() and self._ensemble_name_field.isValid() ) @@ -96,5 +113,5 @@ def get_experiment_arguments(self) -> Arguments: mode=ENSEMBLE_EXPERIMENT_MODE, current_ensemble=self._ensemble_name_field.get_text, realizations=self._active_realizations_field.text(), - experiment_name=self._name_field.get_text, + experiment_name=self._experiment_name_field.get_text, ) diff --git a/src/ert/gui/simulation/ensemble_smoother_panel.py b/src/ert/gui/simulation/ensemble_smoother_panel.py index 3a3f2ad1a53..26361f7a27a 100644 --- a/src/ert/gui/simulation/ensemble_smoother_panel.py +++ b/src/ert/gui/simulation/ensemble_smoother_panel.py @@ -3,17 +3,18 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from qtpy.QtWidgets import QFormLayout, QLabel, QLineEdit +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QFormLayout, QLabel from ert.gui.ertnotifier import ErtNotifier -from ert.gui.ertwidgets import AnalysisModuleEdit +from ert.gui.ertwidgets import AnalysisModuleEdit, StringBox, TextModel from ert.gui.ertwidgets.copyablelabel import CopyableLabel from ert.gui.ertwidgets.models.activerealizationsmodel import ActiveRealizationsModel from ert.gui.ertwidgets.models.targetensemblemodel import TargetEnsembleModel -from ert.gui.ertwidgets.stringbox import StringBox from ert.mode_definitions import ENSEMBLE_SMOOTHER_MODE from ert.run_models import EnsembleSmoother from ert.validation import ProperNameFormatArgument, RangeStringArgument +from ert.validation.range_string_argument import NotInStorage from .experiment_config_panel import ExperimentConfigPanel @@ -44,10 +45,17 @@ def __init__( self.setObjectName("ensemble_smoother_panel") - self._name_field = QLineEdit() - self._name_field.setPlaceholderText(ENSEMBLE_SMOOTHER_MODE) - self._name_field.setMinimumWidth(250) - layout.addRow("Experiment name:", self._name_field) + self._experiment_name_field = StringBox( + TextModel(""), + placeholder_text=self.notifier.storage.get_unique_experiment_name( + ENSEMBLE_SMOOTHER_MODE + ), + ) + self._experiment_name_field.setMinimumWidth(250) + layout.addRow("Experiment name:", self._experiment_name_field) + self._experiment_name_field.setValidator( + NotInStorage(self.notifier.storage, "experiments") + ) runpath_label = CopyableLabel(text=run_path) layout.addRow("Runpath:", runpath_label) @@ -85,6 +93,18 @@ def __init__( self.simulationConfigurationChanged ) + self.notifier.ertChanged.connect(self._update_experiment_name_placeholder) + + @Slot(ExperimentConfigPanel) + def experimentTypeChanged(self, w: ExperimentConfigPanel) -> None: + if isinstance(w, EnsembleSmootherPanel): + self._update_experiment_name_placeholder() + + def _update_experiment_name_placeholder(self) -> None: + self._experiment_name_field.setPlaceholderText( + self.notifier.storage.get_unique_experiment_name(ENSEMBLE_SMOOTHER_MODE) + ) + def isConfigurationValid(self) -> bool: return ( self._ensemble_format_field.isValid() @@ -97,10 +117,6 @@ def get_experiment_arguments(self) -> Arguments: current_ensemble=self._ensemble_format_model.getValue() % 0, target_ensemble=self._ensemble_format_model.getValue() % 1, realizations=self._active_realizations_field.text(), - experiment_name=( - self._name_field.text() - if self._name_field.text() - else self._name_field.placeholderText() - ), + experiment_name=self._experiment_name_field.get_text, ) return arguments diff --git a/src/ert/gui/simulation/experiment_config_panel.py b/src/ert/gui/simulation/experiment_config_panel.py index 8752e2b012c..c223d2bc60c 100644 --- a/src/ert/gui/simulation/experiment_config_panel.py +++ b/src/ert/gui/simulation/experiment_config_panel.py @@ -2,7 +2,7 @@ from typing import Any, Dict -from qtpy.QtCore import Signal +from qtpy.QtCore import Signal, Slot from qtpy.QtWidgets import QWidget @@ -17,10 +17,12 @@ def __init__(self, simulation_model): def get_experiment_type(self): return self.__simulation_model - @staticmethod - def isConfigurationValid() -> bool: + def isConfigurationValid(self) -> bool: return True - @staticmethod - def get_experiment_arguments() -> Dict[str, Any]: + def get_experiment_arguments(self) -> Dict[str, Any]: return {} + + @Slot(QWidget) + def experimentTypeChanged(self, w: QWidget): + pass diff --git a/src/ert/gui/simulation/experiment_panel.py b/src/ert/gui/simulation/experiment_panel.py index 850e513f5ca..ac1bd86cc2d 100644 --- a/src/ert/gui/simulation/experiment_panel.py +++ b/src/ert/gui/simulation/experiment_panel.py @@ -2,7 +2,7 @@ from queue import SimpleQueue from typing import Any, Dict, List -from qtpy.QtCore import QSize, Qt +from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QIcon from qtpy.QtWidgets import ( QAction, @@ -43,6 +43,8 @@ class ExperimentPanel(QWidget): + experiment_type_changed = Signal(ExperimentConfigPanel) + def __init__( self, config: ErtConfig, @@ -68,9 +70,11 @@ def __init__( experiment_type_layout = QHBoxLayout() experiment_type_layout.addSpacing(10) - experiment_type_layout.addWidget(QLabel("Experiment type:"), 0, Qt.AlignVCenter) experiment_type_layout.addWidget( - self._experiment_type_combo, 0, Qt.AlignVCenter + QLabel("Experiment type:"), 0, Qt.AlignmentFlag.AlignVCenter + ) + experiment_type_layout.addWidget( + self._experiment_type_combo, 0, Qt.AlignmentFlag.AlignVCenter ) experiment_type_layout.addSpacing(20) @@ -148,6 +152,7 @@ def addExperimentConfigPanel(self, panel, mode_enabled: bool) -> None: sim_item.setIcon(self.style().standardIcon(QStyle.SP_MessageBoxWarning)) panel.simulationConfigurationChanged.connect(self.validationStatusChanged) + self.experiment_type_changed.connect(panel.experimentTypeChanged) @staticmethod def getActions() -> List[QAction]: @@ -155,7 +160,7 @@ def getActions() -> List[QAction]: def get_current_experiment_type(self): return self._experiment_type_combo.itemData( - self._experiment_type_combo.currentIndex(), Qt.UserRole + self._experiment_type_combo.currentIndex(), Qt.ItemDataRole.UserRole ) def get_experiment_arguments(self) -> Dict[str, Any]: @@ -308,6 +313,7 @@ def toggleExperimentType(self) -> None: widget = self._experiment_widgets[self.get_current_experiment_type()] self._experiment_stack.setCurrentWidget(widget) self.validationStatusChanged() + self.experiment_type_changed.emit(widget) def validationStatusChanged(self) -> None: widget = self._experiment_widgets[self.get_current_experiment_type()] diff --git a/src/ert/gui/simulation/iterated_ensemble_smoother_panel.py b/src/ert/gui/simulation/iterated_ensemble_smoother_panel.py index 56372e4f955..f1bd4e18e9b 100644 --- a/src/ert/gui/simulation/iterated_ensemble_smoother_panel.py +++ b/src/ert/gui/simulation/iterated_ensemble_smoother_panel.py @@ -3,17 +3,18 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from qtpy.QtWidgets import QFormLayout, QLabel, QLineEdit, QSpinBox +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QFormLayout, QLabel, QSpinBox from ert.gui.ertnotifier import ErtNotifier -from ert.gui.ertwidgets import AnalysisModuleEdit +from ert.gui.ertwidgets import AnalysisModuleEdit, StringBox, TextModel from ert.gui.ertwidgets.copyablelabel import CopyableLabel from ert.gui.ertwidgets.models.activerealizationsmodel import ActiveRealizationsModel from ert.gui.ertwidgets.models.targetensemblemodel import TargetEnsembleModel -from ert.gui.ertwidgets.stringbox import StringBox from ert.mode_definitions import ITERATIVE_ENSEMBLE_SMOOTHER_MODE from ert.run_models import IteratedEnsembleSmoother from ert.validation import ProperNameFormatArgument, RangeStringArgument +from ert.validation.range_string_argument import NotInStorage from .experiment_config_panel import ExperimentConfigPanel @@ -43,10 +44,17 @@ def __init__( self.analysis_config = analysis_config layout = QFormLayout() - self._name_field = QLineEdit() - self._name_field.setPlaceholderText("iterated_ensemble_smoother") - self._name_field.setMinimumWidth(250) - layout.addRow("Experiment name:", self._name_field) + self._experiment_name_field = StringBox( + TextModel(""), + placeholder_text=self.notifier.storage.get_unique_experiment_name( + ITERATIVE_ENSEMBLE_SMOOTHER_MODE + ), + ) + self._experiment_name_field.setMinimumWidth(250) + layout.addRow("Experiment name:", self._experiment_name_field) + self._experiment_name_field.setValidator( + NotInStorage(self.notifier.storage, "experiments") + ) runpath_label = CopyableLabel(text=run_path) layout.addRow("Runpath:", runpath_label) @@ -98,6 +106,20 @@ def __init__( ) self.setLayout(layout) + self.notifier.ertChanged.connect(self._update_experiment_name_placeholder) + + @Slot(ExperimentConfigPanel) + def experimentTypeChanged(self, w: ExperimentConfigPanel) -> None: + if isinstance(w, IteratedEnsembleSmootherPanel): + self._update_experiment_name_placeholder() + + def _update_experiment_name_placeholder(self) -> None: + self._experiment_name_field.setPlaceholderText( + self.notifier.storage.get_unique_experiment_name( + ITERATIVE_ENSEMBLE_SMOOTHER_MODE + ) + ) + def setNumberIterations(self, iteration_count): if iteration_count != self.analysis_config.num_iterations: self.analysis_config.set_num_iterations(iteration_count) @@ -115,9 +137,5 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._iterated_target_ensemble_format_model.getValue(), realizations=self._active_realizations_field.text(), num_iterations=self._num_iterations_spinner.value(), - experiment_name=( - self._name_field.text() - if self._name_field.text() - else self._name_field.placeholderText() - ), + experiment_name=self._experiment_name_field.get_text, ) diff --git a/src/ert/gui/simulation/multiple_data_assimilation_panel.py b/src/ert/gui/simulation/multiple_data_assimilation_panel.py index 40e39fca215..4543dd2a5ea 100644 --- a/src/ert/gui/simulation/multiple_data_assimilation_panel.py +++ b/src/ert/gui/simulation/multiple_data_assimilation_panel.py @@ -4,15 +4,20 @@ from typing import TYPE_CHECKING, Any, List from qtpy.QtCore import Slot -from qtpy.QtWidgets import QCheckBox, QFormLayout, QLabel, QLineEdit +from qtpy.QtWidgets import QCheckBox, QFormLayout, QLabel from ert.gui.ertnotifier import ErtNotifier -from ert.gui.ertwidgets import ActiveLabel, AnalysisModuleEdit, EnsembleSelector +from ert.gui.ertwidgets import ( + ActiveLabel, + AnalysisModuleEdit, + EnsembleSelector, + StringBox, + TextModel, +) from ert.gui.ertwidgets.copyablelabel import CopyableLabel from ert.gui.ertwidgets.models.activerealizationsmodel import ActiveRealizationsModel from ert.gui.ertwidgets.models.targetensemblemodel import TargetEnsembleModel from ert.gui.ertwidgets.models.valuemodel import ValueModel -from ert.gui.ertwidgets.stringbox import StringBox from ert.mode_definitions import ES_MDA_MODE from ert.run_models import MultipleDataAssimilation from ert.validation import ( @@ -20,6 +25,7 @@ ProperNameFormatArgument, RangeStringArgument, ) +from ert.validation.range_string_argument import NotInStorage from .experiment_config_panel import ExperimentConfigPanel @@ -52,11 +58,17 @@ def __init__( layout = QFormLayout() self.setObjectName("ES_MDA_panel") - self._experiment_name_field = QLineEdit() - self._experiment_name_field.setPlaceholderText("es_mda") + self._experiment_name_field = StringBox( + TextModel(""), + placeholder_text=self.notifier.storage.get_unique_experiment_name( + ES_MDA_MODE + ), + ) self._experiment_name_field.setMinimumWidth(250) layout.addRow("Experiment name:", self._experiment_name_field) - + self._experiment_name_field.setValidator( + NotInStorage(self.notifier.storage, "experiments") + ) runpath_label = CopyableLabel(text=run_path) layout.addRow("Runpath:", runpath_label) @@ -117,6 +129,18 @@ def __init__( self.setLayout(layout) + self.notifier.ertChanged.connect(self._update_experiment_name_placeholder) + + @Slot(ExperimentConfigPanel) + def experimentTypeChanged(self, w: ExperimentConfigPanel) -> None: + if isinstance(w, MultipleDataAssimilationPanel): + self._update_experiment_name_placeholder() + + def _update_experiment_name_placeholder(self) -> None: + self._experiment_name_field.setPlaceholderText( + self.notifier.storage.get_unique_experiment_name(ES_MDA_MODE) + ) + @Slot() def update_experiment_name(self) -> None: if not self._experiment_name_field.isEnabled(): @@ -199,11 +223,7 @@ def get_experiment_arguments(self) -> Arguments: if self._restart_box.isChecked() else "" ), - experiment_name=( - self._experiment_name_field.text() - if self._experiment_name_field.text() - else self._experiment_name_field.placeholderText() - ), + experiment_name=self._experiment_name_field.get_text, ) def setWeights(self, weights: Any) -> None: diff --git a/src/ert/storage/local_storage.py b/src/ert/storage/local_storage.py index 9647910f5e4..b0f927f9cee 100644 --- a/src/ert/storage/local_storage.py +++ b/src/ert/storage/local_storage.py @@ -490,6 +490,37 @@ def _migrate(self) -> None: ) raise e + def get_unique_experiment_name(self, experiment_name: str) -> str: + """ + Get a unique experiment name + + If an experiment with the given name exists an _0 is appended + or _n+1 where n is the the largest postfix found for the given experiment name + """ + if experiment_name == "": + return self.get_unique_experiment_name("default") + + if experiment_name not in [e.name for e in self.experiments]: + return experiment_name + + if ( + len( + same_prefix := [ + e.name + for e in self.experiments + if e.name.startswith(experiment_name + "_") + ] + ) + > 0 + ): + return ( + experiment_name + + "_" + + str(max(int(e[e.rfind("_") + 1 :]) for e in same_prefix) + 1) + ) + else: + return experiment_name + "_0" + def _storage_version(path: Path) -> Optional[int]: if not path.exists(): diff --git a/tests/unit_tests/gui/simulation/test_run_path_dialog.py b/tests/unit_tests/gui/simulation/test_run_path_dialog.py index d59b5b67637..b4eae840396 100644 --- a/tests/unit_tests/gui/simulation/test_run_path_dialog.py +++ b/tests/unit_tests/gui/simulation/test_run_path_dialog.py @@ -68,7 +68,7 @@ def test_run_path_deleted_error( assert isinstance(simulation_mode_combo, QComboBox) simulation_mode_combo.setCurrentText(EnsembleExperiment.name()) simulation_settings = gui.findChild(EnsembleExperimentPanel) - simulation_settings._name_field.setText("new_experiment_name") + simulation_settings._experiment_name_field.setText("new_experiment_name") # Click start simulation and agree to the message run_experiment = experiment_panel.findChild(QWidget, name="run_experiment") @@ -117,7 +117,7 @@ def test_run_path_is_deleted(snake_oil_case_storage: ErtConfig, qtbot: QtBot): assert isinstance(simulation_mode_combo, QComboBox) simulation_mode_combo.setCurrentText(EnsembleExperiment.name()) simulation_settings = gui.findChild(EnsembleExperimentPanel) - simulation_settings._name_field.setText("new_experiment_name") + simulation_settings._experiment_name_field.setText("new_experiment_name") # Click start simulation and agree to the message run_experiment = experiment_panel.findChild(QWidget, name="run_experiment") @@ -164,7 +164,7 @@ def test_run_path_is_not_deleted(snake_oil_case_storage: ErtConfig, qtbot: QtBot assert isinstance(simulation_mode_combo, QComboBox) simulation_mode_combo.setCurrentText(EnsembleExperiment.name()) simulation_settings = gui.findChild(EnsembleExperimentPanel) - simulation_settings._name_field.setText("new_experiment_name") + simulation_settings._experiment_name_field.setText("new_experiment_name") # Click start simulation and agree to the message run_experiment = experiment_panel.findChild(QWidget, name="run_experiment") diff --git a/tests/unit_tests/storage/test_local_storage.py b/tests/unit_tests/storage/test_local_storage.py index 5f04ab959af..7810f387beb 100644 --- a/tests/unit_tests/storage/test_local_storage.py +++ b/tests/unit_tests/storage/test_local_storage.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List -from unittest.mock import patch +from unittest.mock import MagicMock, PropertyMock, patch from uuid import UUID import hypothesis.strategies as st @@ -338,6 +338,38 @@ def test_remove_and_add_response_from_storage( assert resp.equals(gpr_diff_ds) +def test_get_unique_experiment_name(snake_oil_storage): + with patch( + "ert.storage.local_storage.LocalStorage.experiments", new_callable=PropertyMock + ) as experiments: + # Its not possible to do MagicMock(name="experiment_name") therefore the workaround below + names = [ + "experiment", + "experiment_1", + "experiment_8", + "_d_e_", + "___name__0___", + "__name__1", + "default", + ] + experiment_list = [MagicMock() for _ in range(len(names))] + for k, v in zip(experiment_list, names): + k.name = v + experiments.return_value = experiment_list + + assert snake_oil_storage.get_unique_experiment_name("_d_e_") == "_d_e__0" + assert ( + snake_oil_storage.get_unique_experiment_name("experiment") == "experiment_9" + ) + assert ( + snake_oil_storage.get_unique_experiment_name("___name__0___") + == "___name__0____0" + ) + assert snake_oil_storage.get_unique_experiment_name("name") == "name" + assert snake_oil_storage.get_unique_experiment_name("__name__") == "__name__" + assert snake_oil_storage.get_unique_experiment_name("") == "default_0" + + parameter_configs = st.lists( st.one_of( st.builds(