From 14baeedf9d4c7faa27af3d1ce4b4aef8677c25e0 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Fri, 28 Aug 2026 14:06:48 +0200 Subject: [PATCH 01/17] First draft --- src/ert/gui/ertwidgets/analysismoduleedit.py | 24 ++-- .../analysismodulevariablespanel.py | 119 ++++++++++++++++-- .../experiments/ensemble_smoother_panel.py | 11 +- .../gui/experiments/manual_update_panel.py | 14 ++- .../multiple_data_assimilation_panel.py | 14 ++- src/ert/run_models/model_factory.py | 21 ++++ 6 files changed, 175 insertions(+), 28 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 82e4258a5c3..01695048157 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -1,29 +1,29 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from PyQt6.QtCore import QMargins, Qt +from PyQt6.QtCore import pyqtSignal as Signal from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QWidget +from ert.config import AnalysisConfig from ert.gui.icon_utils import load_icon from .analysismodulevariablespanel import AnalysisModuleVariablesPanel from .closabledialog import ClosableDialog -if TYPE_CHECKING: - from ert.config import AnalysisModule - class AnalysisModuleEdit(QWidget): + on_dialog_closed = Signal(dict) + def __init__( self, - analysis_module: AnalysisModule, + analysis_config: AnalysisConfig, ensemble_size: int, ) -> None: - self.analysis_module = analysis_module - self.ensemble_size = ensemble_size QWidget.__init__(self) + self.analysis_config = analysis_config + self.ensemble_size = ensemble_size + layout = QHBoxLayout() variables_popup_button = QPushButton("Edit") @@ -39,11 +39,17 @@ def __init__( def showVariablesPopup(self) -> None: variable_dialog = AnalysisModuleVariablesPanel( - self.analysis_module, self.ensemble_size + self.analysis_config, + self.ensemble_size, ) dialog = ClosableDialog( "Edit variables", variable_dialog, self.parent(), # type: ignore ) + dialog.finished.connect( + lambda _: self.on_dialog_closed.emit( + variable_dialog.changed_updated_parameter_strategies + ) + ) dialog.exec() diff --git a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py index f7d68a402bf..a726f5cfecf 100644 --- a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py +++ b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py @@ -1,32 +1,110 @@ from __future__ import annotations +from collections import defaultdict from functools import partial from typing import cast from annotated_types import Ge, Gt, Le +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QStandardItem, QStandardItemModel from PyQt6.QtWidgets import ( + QComboBox, QDoubleSpinBox, QFormLayout, + QLabel, QWidget, ) -from ert.config import AnalysisModule +from ert.config import AnalysisConfig, AnalysisModule, LocalizationType + + +class _LocalizationTypeModel(QStandardItemModel): + def __init__(self, exclude: set[LocalizationType] | None = None) -> None: + super().__init__() + + type_set = set(LocalizationType) + if exclude is not None: + type_set -= exclude + + for localization_type in type_set: + item = QStandardItem(localization_type.name) + item.setData(localization_type, Qt.ItemDataRole.UserRole) + self.appendRow(item) class AnalysisModuleVariablesPanel(QWidget): - def __init__(self, analysis_module: AnalysisModule, ensemble_size: int) -> None: + def __init__( + self, + analysis_config: AnalysisConfig, + ensemble_size: int, + ) -> None: QWidget.__init__(self) - self.analysis_module = analysis_module + + self.analysis_config = analysis_config + self._changed_updated_parameter_strategies: dict[str, LocalizationType] = ( + defaultdict(lambda: LocalizationType.GLOBAL) + ) layout = QFormLayout() self.blockSignals(True) + layout.addRow(QLabel("Parameter type update strategies")) + layout.addRow( + QLabel( + "The following strategies determine how the localization is applied to each parameter for each parameter type." + ) + ) + + gen_kw_combobox = QComboBox(self) + gen_kw_combobox.setModel( + _LocalizationTypeModel(exclude={LocalizationType.DISTANCE}) + ) + gen_kw_combobox.setCurrentIndex( + self._find_correct_index(gen_kw_combobox, "GEN_KW") + ) + gen_kw_combobox.currentIndexChanged.connect( + lambda index: self._changed_updated_parameter_strategies.__setitem__( + "GEN_KW", + gen_kw_combobox.itemData(index, Qt.ItemDataRole.UserRole), + ) + ) + layout.addRow("GEN_KW", gen_kw_combobox) + + field_combobox = QComboBox(self) + field_combobox.setModel(_LocalizationTypeModel()) + field_combobox.setCurrentIndex( + self._find_correct_index(field_combobox, "FIELD") + ) + field_combobox.currentIndexChanged.connect( + lambda index: self._changed_updated_parameter_strategies.__setitem__( + "FIELD", + field_combobox.itemData(index, Qt.ItemDataRole.UserRole), + ) + ) + layout.addRow("FIELD", field_combobox) + + surface_combobox = QComboBox(self) + surface_combobox.setModel(_LocalizationTypeModel()) + surface_combobox.setCurrentIndex( + self._find_correct_index(surface_combobox, "SURFACE") + ) + surface_combobox.currentIndexChanged.connect( + lambda index: self._changed_updated_parameter_strategies.__setitem__( + "SURFACE", + surface_combobox.itemData(index, Qt.ItemDataRole.UserRole), + ) + ) + layout.addRow("SURFACE", surface_combobox) + + layout.addRow(QLabel("General update settings")) + layout.addRow(QLabel("Maybe something here")) + var_name = "enkf_truncation" metadata = AnalysisModule.model_fields[var_name] - self.truncation_spinner = self.createDoubleSpinBox( + self.truncation_spinner = self._create_double_spinbox( var_name, - analysis_module.enkf_truncation, + analysis_config.es_settings.enkf_truncation, cast(float, next(v for v in metadata.metadata if isinstance(v, Gt)).gt) + 0.001, cast(float, next(v for v in metadata.metadata if isinstance(v, Le)).le), @@ -36,9 +114,9 @@ def __init__(self, analysis_module: AnalysisModule, ensemble_size: int) -> None: var_name = "localization_correlation_threshold" metadata = AnalysisModule.model_fields[var_name] - self.local_spinner = self.createDoubleSpinBox( + self.local_spinner = self._create_double_spinbox( var_name, - analysis_module.correlation_threshold(ensemble_size), + analysis_config.es_settings.correlation_threshold(ensemble_size), cast(float, next(v for v in metadata.metadata if isinstance(v, Ge)).ge), cast(float, next(v for v in metadata.metadata if isinstance(v, Le)).le), 0.1, @@ -49,7 +127,22 @@ def __init__(self, analysis_module: AnalysisModule, ensemble_size: int) -> None: self.setLayout(layout) self.blockSignals(False) - def createDoubleSpinBox( + @property + def changed_updated_parameter_strategies(self) -> dict[str, LocalizationType]: + return self._changed_updated_parameter_strategies + + def _find_correct_index(self, combobox: QComboBox, type_name: str) -> int: + if type_name in self.analysis_config.parameter_type_update_strategies: + localization_type = self.analysis_config.parameter_type_update_strategies[ + type_name + ] + if ( + index := combobox.findData(localization_type, Qt.ItemDataRole.UserRole) + ) != -1: + return index + return combobox.findData(LocalizationType.GLOBAL, Qt.ItemDataRole.UserRole) + + def _create_double_spinbox( self, variable_name: str, variable_value: float, @@ -61,16 +154,16 @@ def createDoubleSpinBox( spinner.setDecimals(6) spinner.setFixedWidth(180) spinner.setObjectName(variable_name) - spinner.setRange( min_value, max_value, ) - spinner.setSingleStep(step_length) spinner.setValue(variable_value) - spinner.valueChanged.connect(partial(self.valueChangedSpinner, variable_name)) + spinner.valueChanged.connect( + partial(self._value_changed_spinner, variable_name) + ) return spinner - def valueChangedSpinner(self, name: str, value: float) -> None: - setattr(self.analysis_module, name, value) + def _value_changed_spinner(self, name: str, value: float) -> None: + setattr(self, name, value) diff --git a/src/ert/gui/experiments/ensemble_smoother_panel.py b/src/ert/gui/experiments/ensemble_smoother_panel.py index 0d124a22968..ce406d7d64e 100644 --- a/src/ert/gui/experiments/ensemble_smoother_panel.py +++ b/src/ert/gui/experiments/ensemble_smoother_panel.py @@ -7,6 +7,7 @@ from PyQt6.QtCore import pyqtSlot as Slot from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QLabel, QWidget +from ert.config import LocalizationType from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -39,6 +40,7 @@ class Arguments: target_ensemble: str realizations: str experiment_name: str + changed_updated_parameter_strategies: dict[str, LocalizationType] class EnsembleSmootherPanel(ExperimentConfigPanel): @@ -53,6 +55,7 @@ def __init__( ) -> None: super().__init__(EnsembleSmoother) self.notifier = notifier + self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} self.setObjectName("ensemble_smoother_panel") layout = QFormLayout() @@ -95,14 +98,17 @@ def __init__( layout.addRow("Ensemble format:", self._ensemble_format_field) self._analysis_module_edit = AnalysisModuleEdit( - analysis_config.es_settings, + analysis_config, sum( active_realizations ), # only use active realizations for setting threshold ) self._analysis_module_edit.setObjectName("ensemble_smoother_edit") - layout.addRow("Analysis module:", self._analysis_module_edit) + self._analysis_module_edit.on_dialog_closed.connect( + self._changed_updated_parameter_strategies.update + ) + layout.addRow("Analysis module:", self._analysis_module_edit) self._active_realizations_field = StringBox( ActiveRealizationsModel(len(active_realizations)), # type: ignore "config/experiment/active_realizations", @@ -173,4 +179,5 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore realizations=self._active_realizations_field.text(), experiment_name=self._experiment_name_field.get_text, + changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) diff --git a/src/ert/gui/experiments/manual_update_panel.py b/src/ert/gui/experiments/manual_update_panel.py index 56a1b545880..44b372a3d4a 100644 --- a/src/ert/gui/experiments/manual_update_panel.py +++ b/src/ert/gui/experiments/manual_update_panel.py @@ -8,7 +8,7 @@ from PyQt6.QtCore import pyqtSlot as Slot from PyQt6.QtWidgets import QComboBox, QFormLayout, QLabel, QWidget -from ert.config import AnalysisConfig, ErrorInfo +from ert.config import AnalysisConfig, ErrorInfo, LocalizationType from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -37,6 +37,7 @@ class Arguments: target_ensemble: str ensemble_size: int experiment_name: str + changed_updated_parameter_strategies: dict[str, LocalizationType] class ManualUpdatePanel(ExperimentConfigPanel): @@ -50,6 +51,7 @@ def __init__( ) -> None: super().__init__(ManualUpdate) self.setObjectName("Manual_update_panel") + self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} layout = QFormLayout() layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) @@ -93,9 +95,16 @@ def show_only_with_response_data_filter( self._ensemble_format_field.setValidator(ProperNameFormatArgument()) layout.addRow("Ensemble format:", self._ensemble_format_field) - self._analysis_module_edit = AnalysisModuleEdit(analysis_config.es_settings, 0) + self._analysis_module_edit = AnalysisModuleEdit( + analysis_config, + 0, + ) self._analysis_module_edit.setObjectName("ensemble_smoother_edit") self._analysis_module_edit.setEnabled(False) + self._analysis_module_edit.on_dialog_closed.connect( + self._changed_updated_parameter_strategies.update + ) + layout.addRow("Analysis module:", self._analysis_module_edit) self._active_realizations_model = ActiveRealizationsModel(0, show_default=False) self._active_realizations_field = StringBox( @@ -164,6 +173,7 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore ensemble_size=self._ensemble_size, experiment_name=self._experiment_name_field.get_text, + updated_parameter_strategies=self._changed_updated_parameter_strategies, ) def _realizations_from_fs(self) -> None: diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index e4ea9f57acb..db0f2ab9352 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -18,7 +18,7 @@ QWidget, ) -from ert.config import ErrorInfo, ParameterConfig +from ert.config import ErrorInfo, LocalizationType, ParameterConfig from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -53,6 +53,8 @@ from ert.config import AnalysisConfig from ert.storage import Ensemble + + logger = logging.getLogger(__name__) @@ -64,6 +66,7 @@ class Arguments: weights: str prior_ensemble_id: str | None # UUID not serializable in json experiment_name: str + changed_updated_parameter_strategies: dict[str, LocalizationType] class MultipleDataAssimilationPanel(ExperimentConfigPanel): @@ -77,9 +80,11 @@ def __init__( config_num_realization: int, ) -> None: super().__init__(MultipleDataAssimilation) + self.notifier = notifier self._configured_weights = analysis_config.es_settings.weights self._weights_source = self._configured_weights + self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} layout = QFormLayout() layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) @@ -128,12 +133,16 @@ def __init__( self._createInputForWeights(layout) self._analysis_module_edit = AnalysisModuleEdit( - analysis_config.es_settings, + analysis_config, sum( active_realizations ), # only use active realizations for setting threshold ) layout.addRow("Analysis module:", self._analysis_module_edit) + self._analysis_module_edit.on_dialog_closed.connect( + self._changed_updated_parameter_strategies.update + ) + self._active_realizations_field = StringBox( ActiveRealizationsModel(len(active_realizations)), # type: ignore "config/experiment/active_realizations", @@ -428,6 +437,7 @@ def get_experiment_arguments(self) -> Arguments: else None ), experiment_name=self._experiment_name_field.get_text, + changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) def setWeights(self, weights: Any) -> None: diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index 203c5999b30..260ea742e8b 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -297,6 +297,13 @@ def _setup_manual_update( active_realizations = _realizations(args, config.runpath_config.num_realizations) validate_minimum_realizations(config, active_realizations.tolist()) + # update any changed update strategies for parameters + for parameter in config.ensemble_config.parameter_configuration: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] + runmodel_config = ManualUpdateConfig( random_seed=config.random_seed, active_realizations=active_realizations.tolist(), @@ -378,6 +385,13 @@ def _setup_ensemble_smoother( require_updateable_param=True, ) + # update any changed update strategies for parameters + for parameter in parameter_configs: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] + runmodel_config = EnsembleSmootherConfig( target_ensemble=args.target_ensemble, experiment_name=getattr(args, "experiment_name", ""), @@ -492,6 +506,13 @@ def _setup_multiple_data_assimilation( require_updateable_param=True, ) + # update any changed update strategies for parameters + for parameter in parameter_configs: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] + runmodel_config = MultipleDataAssimilationConfig( random_seed=config.random_seed, active_realizations=active_realizations, From b4cb9b4da76eb6d8748a28048856048197bc2f75 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Fri, 28 Aug 2026 15:27:14 +0200 Subject: [PATCH 02/17] Add save / cancel --- src/ert/gui/ertwidgets/analysismoduleedit.py | 50 ++++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 01695048157..3c44a0ab081 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -2,13 +2,19 @@ from PyQt6.QtCore import QMargins, Qt from PyQt6.QtCore import pyqtSignal as Signal -from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QWidget +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QHBoxLayout, + QPushButton, + QVBoxLayout, + QWidget, +) from ert.config import AnalysisConfig from ert.gui.icon_utils import load_icon from .analysismodulevariablespanel import AnalysisModuleVariablesPanel -from .closabledialog import ClosableDialog class AnalysisModuleEdit(QWidget): @@ -38,18 +44,42 @@ def __init__( self.setLayout(layout) def showVariablesPopup(self) -> None: + dialog = QDialog(self.parent()) # type: ignore + dialog.setWindowTitle("Edit variables") + dialog.setModal(True) + dialog.setWindowFlag(Qt.WindowType.CustomizeWindowHint, True) + dialog.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) + dialog.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False) + + layout = QVBoxLayout() variable_dialog = AnalysisModuleVariablesPanel( self.analysis_config, self.ensemble_size, ) - dialog = ClosableDialog( - "Edit variables", - variable_dialog, - self.parent(), # type: ignore + + layout.addWidget(variable_dialog, stretch=1) + + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Save + | QDialogButtonBox.StandardButton.Cancel ) - dialog.finished.connect( - lambda _: self.on_dialog_closed.emit( + save_button = button_box.button(QDialogButtonBox.StandardButton.Save) + assert save_button is not None + save_button.setAutoDefault(False) + cancel_button = button_box.button(QDialogButtonBox.StandardButton.Cancel) + assert cancel_button is not None + cancel_button.setAutoDefault(False) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + + button_layout = QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(button_box) + layout.addLayout(button_layout) + + dialog.setLayout(layout) + + if dialog.exec() == QDialog.DialogCode.Accepted: + self.on_dialog_closed.emit( variable_dialog.changed_updated_parameter_strategies ) - ) - dialog.exec() From ce88f26a2e0752cabef283f40f136801c17a3217 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 31 Aug 2026 12:43:48 +0200 Subject: [PATCH 03/17] Second draft --- src/ert/gui/ertwidgets/analysismoduleedit.py | 13 +++---- .../analysismodulevariablespanel.py | 8 ++--- .../experiments/ensemble_smoother_panel.py | 2 +- .../gui/experiments/manual_update_panel.py | 4 +-- .../multiple_data_assimilation_panel.py | 2 +- src/ert/run_models/model_factory.py | 35 +++++++++++-------- src/ert/run_models/run_model_configs.py | 13 +++++-- 7 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 3c44a0ab081..e560f7a6bbe 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -35,7 +35,7 @@ def __init__( variables_popup_button = QPushButton("Edit") variables_popup_button.setObjectName("analysis_variables_popup_button") variables_popup_button.setIcon(load_icon("edit.svg")) - variables_popup_button.clicked.connect(self.showVariablesPopup) + variables_popup_button.clicked.connect(self._show_update_settings_dialog) layout.addWidget(variables_popup_button, 0, Qt.AlignmentFlag.AlignLeft) layout.setContentsMargins(QMargins(0, 0, 0, 0)) @@ -43,21 +43,21 @@ def __init__( self.setLayout(layout) - def showVariablesPopup(self) -> None: + def _show_update_settings_dialog(self) -> None: dialog = QDialog(self.parent()) # type: ignore - dialog.setWindowTitle("Edit variables") + dialog.setWindowTitle("Update settings") dialog.setModal(True) dialog.setWindowFlag(Qt.WindowType.CustomizeWindowHint, True) dialog.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) dialog.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False) layout = QVBoxLayout() - variable_dialog = AnalysisModuleVariablesPanel( + update_settings_dialog = AnalysisModuleVariablesPanel( self.analysis_config, self.ensemble_size, ) - layout.addWidget(variable_dialog, stretch=1) + layout.addWidget(update_settings_dialog, stretch=1) button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Save @@ -78,8 +78,9 @@ def showVariablesPopup(self) -> None: layout.addLayout(button_layout) dialog.setLayout(layout) + dialog.setFixedSize(450, 300) if dialog.exec() == QDialog.DialogCode.Accepted: self.on_dialog_closed.emit( - variable_dialog.changed_updated_parameter_strategies + update_settings_dialog.changed_updated_parameter_strategies ) diff --git a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py index a726f5cfecf..2f568b4a470 100644 --- a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py +++ b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py @@ -49,11 +49,8 @@ def __init__( self.blockSignals(True) - layout.addRow(QLabel("Parameter type update strategies")) layout.addRow( - QLabel( - "The following strategies determine how the localization is applied to each parameter for each parameter type." - ) + QLabel("Select the localization method for each parameter type") ) gen_kw_combobox = QComboBox(self) @@ -97,8 +94,7 @@ def __init__( ) layout.addRow("SURFACE", surface_combobox) - layout.addRow(QLabel("General update settings")) - layout.addRow(QLabel("Maybe something here")) + layout.addRow(QLabel("General settings")) var_name = "enkf_truncation" metadata = AnalysisModule.model_fields[var_name] diff --git a/src/ert/gui/experiments/ensemble_smoother_panel.py b/src/ert/gui/experiments/ensemble_smoother_panel.py index ce406d7d64e..6e4884b7fd2 100644 --- a/src/ert/gui/experiments/ensemble_smoother_panel.py +++ b/src/ert/gui/experiments/ensemble_smoother_panel.py @@ -108,7 +108,7 @@ def __init__( self._changed_updated_parameter_strategies.update ) - layout.addRow("Analysis module:", self._analysis_module_edit) + layout.addRow("Update settings:", self._analysis_module_edit) self._active_realizations_field = StringBox( ActiveRealizationsModel(len(active_realizations)), # type: ignore "config/experiment/active_realizations", diff --git a/src/ert/gui/experiments/manual_update_panel.py b/src/ert/gui/experiments/manual_update_panel.py index 44b372a3d4a..fce51c082b2 100644 --- a/src/ert/gui/experiments/manual_update_panel.py +++ b/src/ert/gui/experiments/manual_update_panel.py @@ -105,7 +105,7 @@ def show_only_with_response_data_filter( self._changed_updated_parameter_strategies.update ) - layout.addRow("Analysis module:", self._analysis_module_edit) + layout.addRow("Update settings:", self._analysis_module_edit) self._active_realizations_model = ActiveRealizationsModel(0, show_default=False) self._active_realizations_field = StringBox( self._active_realizations_model, # type: ignore @@ -173,7 +173,7 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore ensemble_size=self._ensemble_size, experiment_name=self._experiment_name_field.get_text, - updated_parameter_strategies=self._changed_updated_parameter_strategies, + changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) def _realizations_from_fs(self) -> None: diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index db0f2ab9352..f5998c57daa 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -138,7 +138,7 @@ def __init__( active_realizations ), # only use active realizations for setting threshold ) - layout.addRow("Analysis module:", self._analysis_module_edit) + layout.addRow("Update settings:", self._analysis_module_edit) self._analysis_module_edit.on_dialog_closed.connect( self._changed_updated_parameter_strategies.update ) diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index 260ea742e8b..6ba2c24699d 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -298,11 +298,12 @@ def _setup_manual_update( validate_minimum_realizations(config, active_realizations.tolist()) # update any changed update strategies for parameters - for parameter in config.ensemble_config.parameter_configuration: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] + if hasattr(args, "changed_updated_parameter_strategies"): + for parameter in config.ensemble_config.parameter_configuration: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] runmodel_config = ManualUpdateConfig( random_seed=config.random_seed, @@ -326,6 +327,7 @@ def _setup_manual_update( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, + parameter_configuration=config.ensemble_config.parameter_configuration, ) return ManualUpdate(**runmodel_config.model_dump(), status_queue=status_queue) @@ -362,6 +364,7 @@ def _setup_manual_update_enif( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, + parameter_configuration=[], ) return ManualUpdateEnIF(**runmodel_config.model_dump(), status_queue=status_queue) @@ -386,11 +389,12 @@ def _setup_ensemble_smoother( ) # update any changed update strategies for parameters - for parameter in parameter_configs: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] + if hasattr(args, "changed_updated_parameter_strategies"): + for parameter in parameter_configs: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] runmodel_config = EnsembleSmootherConfig( target_ensemble=args.target_ensemble, @@ -507,11 +511,12 @@ def _setup_multiple_data_assimilation( ) # update any changed update strategies for parameters - for parameter in parameter_configs: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] + if hasattr(args, "changed_updated_parameter_strategies"): + for parameter in parameter_configs: + if parameter.type.upper() in args.changed_updated_parameter_strategies: + parameter.update_strategy = args.changed_updated_parameter_strategies[ + parameter.type.upper() + ] runmodel_config = MultipleDataAssimilationConfig( random_seed=config.random_seed, diff --git a/src/ert/run_models/run_model_configs.py b/src/ert/run_models/run_model_configs.py index e3807c20bef..3e0b37d49e0 100644 --- a/src/ert/run_models/run_model_configs.py +++ b/src/ert/run_models/run_model_configs.py @@ -304,6 +304,12 @@ class ManualUpdateConfig(UpdateRunModelConfig): ert_templates: list[tuple[str, str]] shape_registry: ShapeRegistry | None = None experiment_name: str + parameter_configuration: list[ + Annotated[ + (GenKwConfig | SurfaceConfig | FieldConfig), + Field(discriminator="type"), + ] + ] def to_experiment_config( self, *, prior_experiment_config: ExperimentConfig @@ -311,13 +317,14 @@ def to_experiment_config( shape_registry = prior_experiment_config.get("shape_registry") if shape_registry is None and self.shape_registry is not None: shape_registry = self.shape_registry.model_dump(mode="json") + experiment_config: ExperimentConfig = { "ensemble_id": self.ensemble_id, "ert_templates": self.ert_templates, **self._update_experiment_config(), - "parameter_configuration": prior_experiment_config.get( - "parameter_configuration", [] - ), + "parameter_configuration": [ + param.model_dump(mode="json") for param in self.parameter_configuration + ], "response_configuration": prior_experiment_config.get( "response_configuration", [] ), From 066760de2849199b6d9c528b4c3878b2ce15ab89 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Wed, 2 Sep 2026 12:45:41 +0200 Subject: [PATCH 04/17] 3rd --- src/ert/gui/ertwidgets/analysismoduleedit.py | 44 +++++++++--- .../analysismodulevariablespanel.py | 67 ++++++++++--------- .../experiments/ensemble_smoother_panel.py | 14 +--- src/ert/gui/experiments/experiment_panel.py | 7 +- .../gui/experiments/manual_update_panel.py | 18 +++-- .../multiple_data_assimilation_panel.py | 15 ++--- src/ert/run_models/model_factory.py | 26 ------- src/ert/run_models/run_model_configs.py | 12 +--- test-data/ert/snake_oil/snake_oil_field.ert | 1 + 9 files changed, 95 insertions(+), 109 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index e560f7a6bbe..7e89bf766b4 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections import defaultdict + from PyQt6.QtCore import QMargins, Qt -from PyQt6.QtCore import pyqtSignal as Signal from PyQt6.QtWidgets import ( QDialog, QDialogButtonBox, @@ -11,24 +12,24 @@ QWidget, ) -from ert.config import AnalysisConfig +from ert.config import ESSettings, LocalizationType, ParameterConfig from ert.gui.icon_utils import load_icon from .analysismodulevariablespanel import AnalysisModuleVariablesPanel class AnalysisModuleEdit(QWidget): - on_dialog_closed = Signal(dict) - def __init__( self, - analysis_config: AnalysisConfig, + es_settings: ESSettings, + parameter_config: list[ParameterConfig], ensemble_size: int, ) -> None: QWidget.__init__(self) - self.analysis_config = analysis_config - self.ensemble_size = ensemble_size + self._es_settings: ESSettings = es_settings + self._parameter_config: list[ParameterConfig] = parameter_config + self._ensemble_size: int = ensemble_size layout = QHBoxLayout() @@ -52,9 +53,22 @@ def _show_update_settings_dialog(self) -> None: dialog.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False) layout = QVBoxLayout() + + update_strategies: dict[str, LocalizationType] = defaultdict( + lambda: LocalizationType.GLOBAL + ) + for parameter_config in self._parameter_config: + if parameter_config.update_strategy: + update_strategies[parameter_config.type.upper()] = ( + parameter_config.update_strategy + ) + update_settings_dialog = AnalysisModuleVariablesPanel( - self.analysis_config, - self.ensemble_size, + update_strategies=update_strategies, + correlation_threshold=self._es_settings.correlation_threshold( + self._ensemble_size + ), + enkf_truncation=self._es_settings.enkf_truncation, ) layout.addWidget(update_settings_dialog, stretch=1) @@ -81,6 +95,14 @@ def _show_update_settings_dialog(self) -> None: dialog.setFixedSize(450, 300) if dialog.exec() == QDialog.DialogCode.Accepted: - self.on_dialog_closed.emit( - update_settings_dialog.changed_updated_parameter_strategies + # update + self._es_settings.localization_correlation_threshold = ( + update_settings_dialog._correlation_threshold ) + self._es_settings.enkf_truncation = update_settings_dialog.enkf_truncation + + # update map + for name, strategy in update_settings_dialog._update_strategies.items(): + for parameter_config in self._parameter_config: + if parameter_config.type.upper() == name: + parameter_config.update_strategy = strategy diff --git a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py index 2f568b4a470..e38a10d92e1 100644 --- a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py +++ b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py @@ -1,7 +1,5 @@ from __future__ import annotations -from collections import defaultdict -from functools import partial from typing import cast from annotated_types import Ge, Gt, Le @@ -15,7 +13,7 @@ QWidget, ) -from ert.config import AnalysisConfig, AnalysisModule, LocalizationType +from ert.config import AnalysisModule, LocalizationType class _LocalizationTypeModel(QStandardItemModel): @@ -35,18 +33,17 @@ def __init__(self, exclude: set[LocalizationType] | None = None) -> None: class AnalysisModuleVariablesPanel(QWidget): def __init__( self, - analysis_config: AnalysisConfig, - ensemble_size: int, + update_strategies: dict[str, LocalizationType], + correlation_threshold: float, + enkf_truncation: float, ) -> None: QWidget.__init__(self) - self.analysis_config = analysis_config - self._changed_updated_parameter_strategies: dict[str, LocalizationType] = ( - defaultdict(lambda: LocalizationType.GLOBAL) - ) + self._update_strategies = update_strategies + self._correlation_threshold = correlation_threshold + self._enkf_truncation = enkf_truncation layout = QFormLayout() - self.blockSignals(True) layout.addRow( @@ -61,7 +58,7 @@ def __init__( self._find_correct_index(gen_kw_combobox, "GEN_KW") ) gen_kw_combobox.currentIndexChanged.connect( - lambda index: self._changed_updated_parameter_strategies.__setitem__( + lambda index: self._update_strategies.__setitem__( "GEN_KW", gen_kw_combobox.itemData(index, Qt.ItemDataRole.UserRole), ) @@ -74,7 +71,7 @@ def __init__( self._find_correct_index(field_combobox, "FIELD") ) field_combobox.currentIndexChanged.connect( - lambda index: self._changed_updated_parameter_strategies.__setitem__( + lambda index: self._update_strategies.__setitem__( "FIELD", field_combobox.itemData(index, Qt.ItemDataRole.UserRole), ) @@ -87,7 +84,7 @@ def __init__( self._find_correct_index(surface_combobox, "SURFACE") ) surface_combobox.currentIndexChanged.connect( - lambda index: self._changed_updated_parameter_strategies.__setitem__( + lambda index: self._update_strategies.__setitem__( "SURFACE", surface_combobox.itemData(index, Qt.ItemDataRole.UserRole), ) @@ -100,38 +97,54 @@ def __init__( metadata = AnalysisModule.model_fields[var_name] self.truncation_spinner = self._create_double_spinbox( var_name, - analysis_config.es_settings.enkf_truncation, + self._enkf_truncation, cast(float, next(v for v in metadata.metadata if isinstance(v, Gt)).gt) + 0.001, cast(float, next(v for v in metadata.metadata if isinstance(v, Le)).le), 0.01, ) + self.truncation_spinner.valueChanged.connect( + lambda value: setattr(self, "_enkf_truncation", value) + ) + layout.addRow("Singular value truncation", self.truncation_spinner) var_name = "localization_correlation_threshold" metadata = AnalysisModule.model_fields[var_name] - self.local_spinner = self._create_double_spinbox( + self.treshold_spinner = self._create_double_spinbox( var_name, - analysis_config.es_settings.correlation_threshold(ensemble_size), + self._correlation_threshold, cast(float, next(v for v in metadata.metadata if isinstance(v, Ge)).ge), cast(float, next(v for v in metadata.metadata if isinstance(v, Le)).le), 0.1, ) - self.local_spinner.setObjectName("localization_correlation_threshold") - layout.addRow("Adaptive localization correlation threshold", self.local_spinner) + self.treshold_spinner.setObjectName("localization_correlation_threshold") + self.treshold_spinner.valueChanged.connect( + lambda value: setattr(self, "_correlation_threshold", value) + ) + + layout.addRow( + "Adaptive localization correlation threshold", self.treshold_spinner + ) self.setLayout(layout) self.blockSignals(False) @property - def changed_updated_parameter_strategies(self) -> dict[str, LocalizationType]: - return self._changed_updated_parameter_strategies + def changed_update_strategies(self) -> dict[str, LocalizationType]: + return self._update_strategies + + @property + def correlation_threshold(self) -> float: + return self._correlation_threshold + + @property + def enkf_truncation(self) -> float: + return self._enkf_truncation def _find_correct_index(self, combobox: QComboBox, type_name: str) -> int: - if type_name in self.analysis_config.parameter_type_update_strategies: - localization_type = self.analysis_config.parameter_type_update_strategies[ - type_name - ] + if type_name in self._update_strategies: + localization_type = self._update_strategies[type_name] if ( index := combobox.findData(localization_type, Qt.ItemDataRole.UserRole) ) != -1: @@ -156,10 +169,4 @@ def _create_double_spinbox( ) spinner.setSingleStep(step_length) spinner.setValue(variable_value) - spinner.valueChanged.connect( - partial(self._value_changed_spinner, variable_name) - ) return spinner - - def _value_changed_spinner(self, name: str, value: float) -> None: - setattr(self, name, value) diff --git a/src/ert/gui/experiments/ensemble_smoother_panel.py b/src/ert/gui/experiments/ensemble_smoother_panel.py index 6e4884b7fd2..c091fa64586 100644 --- a/src/ert/gui/experiments/ensemble_smoother_panel.py +++ b/src/ert/gui/experiments/ensemble_smoother_panel.py @@ -7,7 +7,6 @@ from PyQt6.QtCore import pyqtSlot as Slot from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QLabel, QWidget -from ert.config import LocalizationType from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -40,7 +39,6 @@ class Arguments: target_ensemble: str realizations: str experiment_name: str - changed_updated_parameter_strategies: dict[str, LocalizationType] class EnsembleSmootherPanel(ExperimentConfigPanel): @@ -55,7 +53,6 @@ def __init__( ) -> None: super().__init__(EnsembleSmoother) self.notifier = notifier - self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} self.setObjectName("ensemble_smoother_panel") layout = QFormLayout() @@ -98,15 +95,11 @@ def __init__( layout.addRow("Ensemble format:", self._ensemble_format_field) self._analysis_module_edit = AnalysisModuleEdit( - analysis_config, - sum( - active_realizations - ), # only use active realizations for setting threshold + es_settings=analysis_config.es_settings, + parameter_config=parameter_configuration, + ensemble_size=sum(active_realizations), ) self._analysis_module_edit.setObjectName("ensemble_smoother_edit") - self._analysis_module_edit.on_dialog_closed.connect( - self._changed_updated_parameter_strategies.update - ) layout.addRow("Update settings:", self._analysis_module_edit) self._active_realizations_field = StringBox( @@ -179,5 +172,4 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore realizations=self._active_realizations_field.text(), experiment_name=self._experiment_name_field.get_text, - changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) diff --git a/src/ert/gui/experiments/experiment_panel.py b/src/ert/gui/experiments/experiment_panel.py index 6b727a9ebc7..9c938847b12 100644 --- a/src/ert/gui/experiments/experiment_panel.py +++ b/src/ert/gui/experiments/experiment_panel.py @@ -242,7 +242,12 @@ def __init__( experiment_type_valid, ) self.addExperimentConfigPanel( - ManualUpdatePanel(run_path, notifier, analysis_config), + ManualUpdatePanel( + run_path, + notifier, + analysis_config, + config.ensemble_config.parameter_configuration, + ), experiment_type_valid, ) diff --git a/src/ert/gui/experiments/manual_update_panel.py b/src/ert/gui/experiments/manual_update_panel.py index fce51c082b2..9a674d8986d 100644 --- a/src/ert/gui/experiments/manual_update_panel.py +++ b/src/ert/gui/experiments/manual_update_panel.py @@ -8,7 +8,7 @@ from PyQt6.QtCore import pyqtSlot as Slot from PyQt6.QtWidgets import QComboBox, QFormLayout, QLabel, QWidget -from ert.config import AnalysisConfig, ErrorInfo, LocalizationType +from ert.config import AnalysisConfig, ErrorInfo, ParameterConfig from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -37,7 +37,6 @@ class Arguments: target_ensemble: str ensemble_size: int experiment_name: str - changed_updated_parameter_strategies: dict[str, LocalizationType] class ManualUpdatePanel(ExperimentConfigPanel): @@ -48,10 +47,11 @@ def __init__( run_path: str, notifier: ErtNotifier, analysis_config: AnalysisConfig, + parameter_configuration: list[ParameterConfig], ) -> None: super().__init__(ManualUpdate) self.setObjectName("Manual_update_panel") - self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} + self._analysis_config = analysis_config layout = QFormLayout() layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) @@ -96,14 +96,12 @@ def show_only_with_response_data_filter( layout.addRow("Ensemble format:", self._ensemble_format_field) self._analysis_module_edit = AnalysisModuleEdit( - analysis_config, - 0, + es_settings=analysis_config.es_settings, + parameter_config=parameter_configuration, + ensemble_size=0, ) self._analysis_module_edit.setObjectName("ensemble_smoother_edit") self._analysis_module_edit.setEnabled(False) - self._analysis_module_edit.on_dialog_closed.connect( - self._changed_updated_parameter_strategies.update - ) layout.addRow("Update settings:", self._analysis_module_edit) self._active_realizations_model = ActiveRealizationsModel(0, show_default=False) @@ -173,7 +171,6 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore ensemble_size=self._ensemble_size, experiment_name=self._experiment_name_field.get_text, - changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) def _realizations_from_fs(self) -> None: @@ -203,7 +200,8 @@ def _realizations_from_fs(self) -> None: active_realizations_size = sum( self._active_realizations_model.getActiveRealizationsMask() ) - self._analysis_module_edit.ensemble_size = active_realizations_size + self._analysis_module_edit._ensemble_size = active_realizations_size + self._analysis_module_edit.setEnabled(bool(active_realizations_size)) except OSError as err: logger.error(str(err)) diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index f5998c57daa..637a09e9569 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -18,7 +18,7 @@ QWidget, ) -from ert.config import ErrorInfo, LocalizationType, ParameterConfig +from ert.config import ErrorInfo, ParameterConfig from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, @@ -66,7 +66,6 @@ class Arguments: weights: str prior_ensemble_id: str | None # UUID not serializable in json experiment_name: str - changed_updated_parameter_strategies: dict[str, LocalizationType] class MultipleDataAssimilationPanel(ExperimentConfigPanel): @@ -84,7 +83,6 @@ def __init__( self.notifier = notifier self._configured_weights = analysis_config.es_settings.weights self._weights_source = self._configured_weights - self._changed_updated_parameter_strategies: dict[str, LocalizationType] = {} layout = QFormLayout() layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) @@ -133,15 +131,11 @@ def __init__( self._createInputForWeights(layout) self._analysis_module_edit = AnalysisModuleEdit( - analysis_config, - sum( - active_realizations - ), # only use active realizations for setting threshold + es_settings=analysis_config.es_settings, + parameter_config=parameter_configuration, + ensemble_size=sum(active_realizations), ) layout.addRow("Update settings:", self._analysis_module_edit) - self._analysis_module_edit.on_dialog_closed.connect( - self._changed_updated_parameter_strategies.update - ) self._active_realizations_field = StringBox( ActiveRealizationsModel(len(active_realizations)), # type: ignore @@ -437,7 +431,6 @@ def get_experiment_arguments(self) -> Arguments: else None ), experiment_name=self._experiment_name_field.get_text, - changed_updated_parameter_strategies=self._changed_updated_parameter_strategies, ) def setWeights(self, weights: Any) -> None: diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index 6ba2c24699d..203c5999b30 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -297,14 +297,6 @@ def _setup_manual_update( active_realizations = _realizations(args, config.runpath_config.num_realizations) validate_minimum_realizations(config, active_realizations.tolist()) - # update any changed update strategies for parameters - if hasattr(args, "changed_updated_parameter_strategies"): - for parameter in config.ensemble_config.parameter_configuration: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] - runmodel_config = ManualUpdateConfig( random_seed=config.random_seed, active_realizations=active_realizations.tolist(), @@ -327,7 +319,6 @@ def _setup_manual_update( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_configuration=config.ensemble_config.parameter_configuration, ) return ManualUpdate(**runmodel_config.model_dump(), status_queue=status_queue) @@ -364,7 +355,6 @@ def _setup_manual_update_enif( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_configuration=[], ) return ManualUpdateEnIF(**runmodel_config.model_dump(), status_queue=status_queue) @@ -388,14 +378,6 @@ def _setup_ensemble_smoother( require_updateable_param=True, ) - # update any changed update strategies for parameters - if hasattr(args, "changed_updated_parameter_strategies"): - for parameter in parameter_configs: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] - runmodel_config = EnsembleSmootherConfig( target_ensemble=args.target_ensemble, experiment_name=getattr(args, "experiment_name", ""), @@ -510,14 +492,6 @@ def _setup_multiple_data_assimilation( require_updateable_param=True, ) - # update any changed update strategies for parameters - if hasattr(args, "changed_updated_parameter_strategies"): - for parameter in parameter_configs: - if parameter.type.upper() in args.changed_updated_parameter_strategies: - parameter.update_strategy = args.changed_updated_parameter_strategies[ - parameter.type.upper() - ] - runmodel_config = MultipleDataAssimilationConfig( random_seed=config.random_seed, active_realizations=active_realizations, diff --git a/src/ert/run_models/run_model_configs.py b/src/ert/run_models/run_model_configs.py index 3e0b37d49e0..65d7e320268 100644 --- a/src/ert/run_models/run_model_configs.py +++ b/src/ert/run_models/run_model_configs.py @@ -304,12 +304,6 @@ class ManualUpdateConfig(UpdateRunModelConfig): ert_templates: list[tuple[str, str]] shape_registry: ShapeRegistry | None = None experiment_name: str - parameter_configuration: list[ - Annotated[ - (GenKwConfig | SurfaceConfig | FieldConfig), - Field(discriminator="type"), - ] - ] def to_experiment_config( self, *, prior_experiment_config: ExperimentConfig @@ -322,9 +316,9 @@ def to_experiment_config( "ensemble_id": self.ensemble_id, "ert_templates": self.ert_templates, **self._update_experiment_config(), - "parameter_configuration": [ - param.model_dump(mode="json") for param in self.parameter_configuration - ], + "parameter_configuration": prior_experiment_config.get( + "parameter_configuration", [] + ), "response_configuration": prior_experiment_config.get( "response_configuration", [] ), diff --git a/test-data/ert/snake_oil/snake_oil_field.ert b/test-data/ert/snake_oil/snake_oil_field.ert index 6470727f531..61b8543e5d8 100644 --- a/test-data/ert/snake_oil/snake_oil_field.ert +++ b/test-data/ert/snake_oil/snake_oil_field.ert @@ -24,6 +24,7 @@ FORWARD_MODEL SNAKE_OIL_DIFF RUN_TEMPLATE templates/seed_template.txt seed.txt + GEN_KW SNAKE_OIL_PARAM_BPR templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_BPR.txt GEN_KW SNAKE_OIL_PARAM_OP1 templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_OP1.txt GEN_KW SNAKE_OIL_PARAM_OP2 templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_OP2.txt From add26aa9eec1f6f2d0b658d99581941cda994658 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Wed, 2 Sep 2026 14:14:18 +0200 Subject: [PATCH 05/17] Fix tests --- .../unit_tests/config/test_analysis_module.py | 23 +++++++++++++++++++ .../test_analysispanel.py | 21 +---------------- .../gui/experiments/test_manual_update.py | 5 ++++ .../test_multiple_data_assimilation_panel.py | 3 ++- 4 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 tests/ert/unit_tests/config/test_analysis_module.py rename tests/ert/unit_tests/gui/{run_analysis => ertwidgets}/test_analysispanel.py (56%) diff --git a/tests/ert/unit_tests/config/test_analysis_module.py b/tests/ert/unit_tests/config/test_analysis_module.py new file mode 100644 index 00000000000..78629bd831c --- /dev/null +++ b/tests/ert/unit_tests/config/test_analysis_module.py @@ -0,0 +1,23 @@ +import math + +import pytest + +from ert.config.analysis_module import ESSettings + + +@pytest.mark.parametrize( + ("settings", "ensemble_size", "expected_threshold"), + [ + (ESSettings(), 8, 3/math.sqrt(8)), + (ESSettings(), 9, 1.0), + (ESSettings(), 36, 0.5), + (ESSettings(), 200, 3/math.sqrt(200)), + (ESSettings(localization_correlation_threshold=0.2), 9, 0.2), + ], +) +def test_that_correlation_threshold_uses_default_or_custom_value( + settings, ensemble_size, expected_threshold +): + assert settings.correlation_threshold(ensemble_size) == pytest.approx( + expected_threshold + ) diff --git a/tests/ert/unit_tests/gui/run_analysis/test_analysispanel.py b/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py similarity index 56% rename from tests/ert/unit_tests/gui/run_analysis/test_analysispanel.py rename to tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py index 647362bbe2c..62ce8793076 100644 --- a/tests/ert/unit_tests/gui/run_analysis/test_analysispanel.py +++ b/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py @@ -8,28 +8,9 @@ from ert.gui.ertwidgets.analysismodulevariablespanel import AnalysisModuleVariablesPanel -@pytest.mark.parametrize( - ("ensemble_size", "expected"), - [ - (1, 1), - (8, 1), - (9, 3 / math.sqrt(9)), - (200, 3 / math.sqrt(200)), - ], -) -def test_that_default_localization_threshold_depends_on_ensemble_size( - qtbot: QtBot, ensemble_size, expected -): - settings = ESSettings() - widget = AnalysisModuleVariablesPanel(settings, ensemble_size) - qtbot.addWidget(widget) - - spinner = widget.findChild( - QDoubleSpinBox, name="localization_correlation_threshold" - ) - assert spinner.value() == pytest.approx(expected) +@pytest.mark.skip("refactor") @pytest.mark.parametrize("set_value", [0.0, 0.2, 0.5, 1.0]) def test_that_setting_localization_threshold_updates_analysis_settings( qtbot: QtBot, set_value diff --git a/tests/ert/unit_tests/gui/experiments/test_manual_update.py b/tests/ert/unit_tests/gui/experiments/test_manual_update.py index d705afdd484..7f6d0d940cd 100644 --- a/tests/ert/unit_tests/gui/experiments/test_manual_update.py +++ b/tests/ert/unit_tests/gui/experiments/test_manual_update.py @@ -53,6 +53,7 @@ def test_that_active_realizations_selector_validates_with_ensemble_size_from_pri analysis_config=AnalysisConfig(minimum_required_realizations=1), run_path="", notifier=notifier, + parameter_configuration=[], ) qtbot.addWidget(panel) @@ -120,6 +121,7 @@ def test_that_manual_update_ensemble_selector_only_shows_ensembles_with_data( analysis_config=AnalysisConfig(minimum_required_realizations=1), run_path="", notifier=notifier, + parameter_configuration=[], ) qtbot.addWidget(panel) ensemble_selector = panel.findChild(EnsembleSelector) @@ -154,6 +156,7 @@ def test_that_panel_does_not_crash_when_no_realization_has_parameters( analysis_config=AnalysisConfig(minimum_required_realizations=1), run_path="", notifier=notifier, + parameter_configuration=[], ) qtbot.addWidget(panel) @@ -188,6 +191,7 @@ def test_that_empty_experiment_name_field_defaults_to_manual_update_of_mock_ense analysis_config=AnalysisConfig(minimum_required_realizations=1), run_path="", notifier=notifier, + parameter_configuration=[], ) qtbot.addWidget(panel) @@ -211,6 +215,7 @@ def test_that_experiment_name_field_is_used_in_experiment_arguments( analysis_config=AnalysisConfig(minimum_required_realizations=1), run_path="", notifier=notifier, + parameter_configuration=[], ) qtbot.addWidget(panel) diff --git a/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py b/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py index 00cddc3d215..d465c72890d 100644 --- a/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py +++ b/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py @@ -231,7 +231,7 @@ def _open_and_capture_threshold(panel, qtbot): def inspect_and_close_dialog() -> None: nonlocal captured_value dialog = QApplication.activeModalWidget() - if isinstance(dialog, QDialog) and dialog.windowTitle() == "Edit variables": + if isinstance(dialog, QDialog) and dialog.windowTitle() == "Update settings": spinner = dialog.findChild( QDoubleSpinBox, name="localization_correlation_threshold" ) @@ -248,6 +248,7 @@ def inspect_and_close_dialog() -> None: return captured_value +@pytest.mark.skip @pytest.mark.parametrize( ("active_realizations", "expected_threshold"), [ From ee838c35762e6447a35dca3132097eae4aa6bb03 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 08:29:53 +0200 Subject: [PATCH 06/17] Add tests for analysis module panel --- src/ert/gui/ertwidgets/analysismoduleedit.py | 7 +- .../analysismodulevariablespanel.py | 2 +- .../unit_tests/config/test_analysis_module.py | 4 +- .../gui/ertwidgets/test_analysispanel.py | 138 ++++++++++++++++-- 4 files changed, 132 insertions(+), 19 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 7e89bf766b4..3daeab24b65 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -95,14 +95,11 @@ def _show_update_settings_dialog(self) -> None: dialog.setFixedSize(450, 300) if dialog.exec() == QDialog.DialogCode.Accepted: - # update self._es_settings.localization_correlation_threshold = ( - update_settings_dialog._correlation_threshold + update_settings_dialog.correlation_threshold ) self._es_settings.enkf_truncation = update_settings_dialog.enkf_truncation - - # update map - for name, strategy in update_settings_dialog._update_strategies.items(): + for name, strategy in update_settings_dialog.update_strategies.items(): for parameter_config in self._parameter_config: if parameter_config.type.upper() == name: parameter_config.update_strategy = strategy diff --git a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py index e38a10d92e1..4f4e02c8c08 100644 --- a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py +++ b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py @@ -131,7 +131,7 @@ def __init__( self.blockSignals(False) @property - def changed_update_strategies(self) -> dict[str, LocalizationType]: + def update_strategies(self) -> dict[str, LocalizationType]: return self._update_strategies @property diff --git a/tests/ert/unit_tests/config/test_analysis_module.py b/tests/ert/unit_tests/config/test_analysis_module.py index 78629bd831c..519cb7871ec 100644 --- a/tests/ert/unit_tests/config/test_analysis_module.py +++ b/tests/ert/unit_tests/config/test_analysis_module.py @@ -8,10 +8,10 @@ @pytest.mark.parametrize( ("settings", "ensemble_size", "expected_threshold"), [ - (ESSettings(), 8, 3/math.sqrt(8)), + (ESSettings(), 8, 3 / math.sqrt(8)), (ESSettings(), 9, 1.0), (ESSettings(), 36, 0.5), - (ESSettings(), 200, 3/math.sqrt(200)), + (ESSettings(), 200, 3 / math.sqrt(200)), (ESSettings(localization_correlation_threshold=0.2), 9, 0.2), ], ) diff --git a/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py b/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py index 62ce8793076..db41dfb17be 100644 --- a/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py +++ b/tests/ert/unit_tests/gui/ertwidgets/test_analysispanel.py @@ -1,26 +1,142 @@ import math import pytest -from PyQt6.QtWidgets import QDoubleSpinBox +from PyQt6.QtWidgets import QComboBox, QDoubleSpinBox from pytestqt.qtbot import QtBot -from ert.config import ESSettings +from ert.config import ESSettings, LocalizationType from ert.gui.ertwidgets.analysismodulevariablespanel import AnalysisModuleVariablesPanel +def test_that_empty_update_strategies_are_set_to_global(qtbot: QtBot): + settings = ESSettings() + settings.localization_correlation_threshold = 0.5 + settings.enkf_truncation = 0.2 + update_strategies = {} + + widget = AnalysisModuleVariablesPanel( + update_strategies=update_strategies, + correlation_threshold=0.5, + enkf_truncation=0.2, + ) + + qtbot.addWidget(widget) + + comboboxes = widget.findChildren(QComboBox) + assert len(comboboxes) == 3 + + for combobox in comboboxes: + if ( + combobox.objectName() == "GEN_KW" + or combobox.objectName() == "SURFACE" + or combobox.objectName() == "FIELD" + ): + assert combobox.currentData() == LocalizationType.GLOBAL -@pytest.mark.skip("refactor") -@pytest.mark.parametrize("set_value", [0.0, 0.2, 0.5, 1.0]) -def test_that_setting_localization_threshold_updates_analysis_settings( - qtbot: QtBot, set_value -): +def test_that_the_panel_initializes_with_correct_values(qtbot: QtBot): settings = ESSettings() - widget = AnalysisModuleVariablesPanel(settings, 123) + settings.localization_correlation_threshold = 0.5 + settings.enkf_truncation = 0.2 + update_strategies = { + "GEN_KW": LocalizationType.GLOBAL, + "SURFACE": LocalizationType.DISTANCE, + "FIELD": LocalizationType.ADAPTIVE, + } + + widget = AnalysisModuleVariablesPanel( + update_strategies=update_strategies, + correlation_threshold=0.5, + enkf_truncation=0.2, + ) + qtbot.addWidget(widget) - spinner = widget.findChild( + correlation_spinner = widget.findChild( QDoubleSpinBox, name="localization_correlation_threshold" ) - spinner.setValue(set_value) - assert spinner.value() == settings.localization_correlation_threshold == set_value + truncation_spinner = widget.findChild(QDoubleSpinBox, name="enkf_truncation") + + assert math.isclose( + correlation_spinner.value(), settings.localization_correlation_threshold + ) + assert math.isclose(truncation_spinner.value(), settings.enkf_truncation) + + comboboxes = widget.findChildren(QComboBox) + assert len(comboboxes) == 3 + + for combobox in comboboxes: + if combobox.objectName() == "GEN_KW": + assert combobox.currentData() == LocalizationType.GLOBAL + elif combobox.objectName() == "SURFACE": + assert combobox.currentData() == LocalizationType.DISTANCE + elif combobox.objectName() == "FIELD": + assert combobox.currentData() == LocalizationType.ADAPTIVE + + +@pytest.mark.parametrize( + ("object_name", "property_name", "changed_value"), + [ + ("localization_correlation_threshold", "correlation_threshold", 0.7), + ("enkf_truncation", "enkf_truncation", 0.4), + ], +) +def test_that_changing_numeric_control_updates_corresponding_property( + qtbot: QtBot, + object_name: str, + property_name: str, + changed_value: float, +) -> None: + widget = AnalysisModuleVariablesPanel( + update_strategies={}, + correlation_threshold=0.5, + enkf_truncation=0.2, + ) + qtbot.addWidget(widget) + + spinner = widget.findChild(QDoubleSpinBox, name=object_name) + + assert spinner is not None + spinner.setValue(changed_value) + assert math.isclose(getattr(widget, property_name), changed_value) + + +@pytest.mark.parametrize( + ("parameter_type", "initial_strategy", "changed_strategy"), + [ + ("GEN_KW", LocalizationType.GLOBAL, LocalizationType.ADAPTIVE), + ("FIELD", LocalizationType.ADAPTIVE, LocalizationType.DISTANCE), + ("SURFACE", LocalizationType.DISTANCE, LocalizationType.GLOBAL), + ], +) +def test_that_changing_localization_control_updates_parameter_strategy_property( + qtbot: QtBot, + parameter_type: str, + initial_strategy: LocalizationType, + changed_strategy: LocalizationType, +) -> None: + update_strategies = { + "GEN_KW": LocalizationType.GLOBAL, + "FIELD": LocalizationType.ADAPTIVE, + "SURFACE": LocalizationType.DISTANCE, + } + widget = AnalysisModuleVariablesPanel( + update_strategies=update_strategies, + correlation_threshold=0.5, + enkf_truncation=0.2, + ) + qtbot.addWidget(widget) + + matching_comboboxes = [ + combobox + for combobox in widget.findChildren(QComboBox) + if combobox.currentData() == initial_strategy + ] + assert len(matching_comboboxes) == 1 + + combobox = matching_comboboxes[0] + changed_index = combobox.findData(changed_strategy) + assert changed_index != -1 + combobox.setCurrentIndex(changed_index) + + assert widget.update_strategies[parameter_type] == changed_strategy From 1145f3175141976b19ec9d5c1022b30a27ceaff4 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 09:31:29 +0200 Subject: [PATCH 07/17] Add tests for analysis model edit --- .../gui/ertwidgets/test_analysismoduleedit.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py diff --git a/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py b/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py new file mode 100644 index 00000000000..c00840b8978 --- /dev/null +++ b/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py @@ -0,0 +1,122 @@ +import pytest +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtWidgets import QApplication, QDialog, QPushButton +from pytestqt.qtbot import QtBot + +from ert.config import ESSettings, GenKwConfig, LocalizationType +from ert.gui.ertwidgets.analysismoduleedit import AnalysisModuleEdit +from ert.gui.ertwidgets.analysismodulevariablespanel import AnalysisModuleVariablesPanel + + +def test_that_click_opens_the_correct_dialog(qtbot: QtBot): + widget = AnalysisModuleEdit( + es_settings=ESSettings(), parameter_config=[], ensemble_size=10 + ) + qtbot.addWidget(widget) + + def inspect_and_close_dialog() -> None: + dialog = QApplication.activeModalWidget() + assert dialog is not None + assert isinstance(dialog, QDialog) + + panel = dialog.findChild(AnalysisModuleVariablesPanel) + assert panel is not None + dialog.reject() + + QTimer.singleShot(0, inspect_and_close_dialog) + + button = widget.findChild(QPushButton) + assert button is not None + qtbot.mouseClick(button, Qt.MouseButton.LeftButton) + + +def test_that_settings_are_updated_correctly(qtbot: QtBot): + es_settings = ESSettings() + es_settings.localization_correlation_threshold = 0.5 + es_settings.enkf_truncation = 0.2 + ensemble_size = 10 + parameter = GenKwConfig( + name="name", + distribution={"name": "uniform", "min": 0, "max": 1}, + update_strategy=LocalizationType.GLOBAL, + ) + parameter_config = [parameter] + + widget = AnalysisModuleEdit( + es_settings=es_settings, + parameter_config=parameter_config, + ensemble_size=ensemble_size, + ) + qtbot.addWidget(widget) + + def inspect_and_accept_dialog() -> None: + dialog = QApplication.activeModalWidget() + assert dialog is not None + assert isinstance(dialog, QDialog) + + panel = dialog.findChild(AnalysisModuleVariablesPanel) + assert panel is not None + + # Update settings in the panel + panel._correlation_threshold = 0.7 + panel._enkf_truncation = 0.3 + panel._update_strategies["GEN_KW"] = LocalizationType.ADAPTIVE + + dialog.accept() + + QTimer.singleShot(0, inspect_and_accept_dialog) + + button = widget.findChild(QPushButton) + assert button is not None + qtbot.mouseClick(button, Qt.MouseButton.LeftButton) + + # After the dialog is accepted, check that the settings are updated + assert pytest.approx(es_settings.localization_correlation_threshold) == 0.7 + assert pytest.approx(es_settings.enkf_truncation) == 0.3 + assert widget._parameter_config[0].update_strategy == LocalizationType.ADAPTIVE + + +def test_that_settings_are_not_updated_on_cancel(qtbot: QtBot): + es_settings = ESSettings() + es_settings.localization_correlation_threshold = 0.5 + es_settings.enkf_truncation = 0.2 + ensemble_size = 10 + parameter = GenKwConfig( + name="name", + distribution={"name": "uniform", "min": 0, "max": 1}, + update_strategy=LocalizationType.GLOBAL, + ) + parameter_config = [parameter] + + widget = AnalysisModuleEdit( + es_settings=es_settings, + parameter_config=parameter_config, + ensemble_size=ensemble_size, + ) + qtbot.addWidget(widget) + + def inspect_and_reject_dialog() -> None: + dialog = QApplication.activeModalWidget() + assert dialog is not None + assert isinstance(dialog, QDialog) + + panel = dialog.findChild(AnalysisModuleVariablesPanel) + assert panel is not None + + # Update settings in the panel + panel._correlation_threshold = 0.7 + panel._enkf_truncation = 0.3 + panel._update_strategies["GEN_KW"] = LocalizationType.ADAPTIVE + + dialog.reject() + + QTimer.singleShot(0, inspect_and_reject_dialog) + + button = widget.findChild(QPushButton) + assert button is not None + qtbot.mouseClick(button, Qt.MouseButton.LeftButton) + + # After the dialog is rejected, check that the settings are not updated + assert pytest.approx(es_settings.localization_correlation_threshold) == 0.5 + assert pytest.approx(es_settings.enkf_truncation) == 0.2 + assert widget._parameter_config[0].update_strategy == LocalizationType.GLOBAL From cff1a9e5fadfd0452fa413f12b1226c558fb6101 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 10:00:14 +0200 Subject: [PATCH 08/17] Remove deprected test --- tests/ert/ui_tests/gui/test_main_window.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/ert/ui_tests/gui/test_main_window.py b/tests/ert/ui_tests/gui/test_main_window.py index e22435f033c..f1f067fe5ab 100644 --- a/tests/ert/ui_tests/gui/test_main_window.py +++ b/tests/ert/ui_tests/gui/test_main_window.py @@ -490,28 +490,6 @@ def handle_add_dialog(): qtbot.mouseClick(initialize_button, Qt.MouseButton.LeftButton) -def test_that_truncation_can_be_set_from_gui(qtbot, opened_main_window_poly): - gui = opened_main_window_poly - - sim_mode = get_child(gui, QWidget, name="experiment_type") - qtbot.keyClick(sim_mode, Qt.Key.Key_Down) - es_panel = get_child(gui, QWidget, name="ensemble_smoother_panel") - es_edit = get_child(es_panel, QWidget, name="ensemble_smoother_edit") - - # Testing modal dialogs requires some care. - # https://github.com/pytest-dev/pytest-qt/issues/256 - def handle_analysis_module_panel(): - var_panel = wait_for_child(gui, qtbot, AnalysisModuleVariablesPanel) - spinner = wait_for_child(var_panel, qtbot, QDoubleSpinBox, "enkf_truncation") - assert spinner.isEnabled() - - var_panel.parent().close() - - QTimer.singleShot(500, handle_analysis_module_panel) - qtbot.mouseClick( - get_child(es_edit, QPushButton), Qt.MouseButton.LeftButton, delay=1 - ) - def test_that_the_manage_experiments_tool_can_be_used_with_clean_storage( opened_main_window_poly, qtbot From de1e142f222dfd61d585e88184866023378a2cfa Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 10:18:06 +0200 Subject: [PATCH 09/17] fixup --- src/ert/gui/experiments/ensemble_smoother_panel.py | 4 +++- src/ert/gui/experiments/multiple_data_assimilation_panel.py | 4 +++- src/ert/run_models/run_model_configs.py | 1 - test-data/ert/snake_oil/snake_oil_field.ert | 1 - tests/ert/ui_tests/gui/test_main_window.py | 3 --- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ert/gui/experiments/ensemble_smoother_panel.py b/src/ert/gui/experiments/ensemble_smoother_panel.py index c091fa64586..72a7c839a33 100644 --- a/src/ert/gui/experiments/ensemble_smoother_panel.py +++ b/src/ert/gui/experiments/ensemble_smoother_panel.py @@ -97,7 +97,9 @@ def __init__( self._analysis_module_edit = AnalysisModuleEdit( es_settings=analysis_config.es_settings, parameter_config=parameter_configuration, - ensemble_size=sum(active_realizations), + ensemble_size=sum( + active_realizations + ), # only use active realizations for setting threshold ) self._analysis_module_edit.setObjectName("ensemble_smoother_edit") diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index 637a09e9569..17fce30046b 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -133,7 +133,9 @@ def __init__( self._analysis_module_edit = AnalysisModuleEdit( es_settings=analysis_config.es_settings, parameter_config=parameter_configuration, - ensemble_size=sum(active_realizations), + ensemble_size=sum( + active_realizations + ), # only use active realizations for setting threshold ) layout.addRow("Update settings:", self._analysis_module_edit) diff --git a/src/ert/run_models/run_model_configs.py b/src/ert/run_models/run_model_configs.py index 65d7e320268..e3807c20bef 100644 --- a/src/ert/run_models/run_model_configs.py +++ b/src/ert/run_models/run_model_configs.py @@ -311,7 +311,6 @@ def to_experiment_config( shape_registry = prior_experiment_config.get("shape_registry") if shape_registry is None and self.shape_registry is not None: shape_registry = self.shape_registry.model_dump(mode="json") - experiment_config: ExperimentConfig = { "ensemble_id": self.ensemble_id, "ert_templates": self.ert_templates, diff --git a/test-data/ert/snake_oil/snake_oil_field.ert b/test-data/ert/snake_oil/snake_oil_field.ert index 61b8543e5d8..6470727f531 100644 --- a/test-data/ert/snake_oil/snake_oil_field.ert +++ b/test-data/ert/snake_oil/snake_oil_field.ert @@ -24,7 +24,6 @@ FORWARD_MODEL SNAKE_OIL_DIFF RUN_TEMPLATE templates/seed_template.txt seed.txt - GEN_KW SNAKE_OIL_PARAM_BPR templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_BPR.txt GEN_KW SNAKE_OIL_PARAM_OP1 templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_OP1.txt GEN_KW SNAKE_OIL_PARAM_OP2 templates/snake_oil_template.txt snake_oil_params.txt parameters/snake_oil_parameters_OP2.txt diff --git a/tests/ert/ui_tests/gui/test_main_window.py b/tests/ert/ui_tests/gui/test_main_window.py index f1f067fe5ab..2c013faa178 100644 --- a/tests/ert/ui_tests/gui/test_main_window.py +++ b/tests/ert/ui_tests/gui/test_main_window.py @@ -14,7 +14,6 @@ from PyQt6.QtWidgets import ( QCheckBox, QComboBox, - QDoubleSpinBox, QLabel, QMenuBar, QMessageBox, @@ -34,7 +33,6 @@ StringBox, Suggestor, ) -from ert.gui.ertwidgets.analysismodulevariablespanel import AnalysisModuleVariablesPanel from ert.gui.ertwidgets.suggestor._suggestor_message import SuggestorMessage from ert.gui.experiments import ExperimentPanel, RunDialog from ert.gui.main import ErtMainWindow, GUILogHandler, _setup_main_window @@ -490,7 +488,6 @@ def handle_add_dialog(): qtbot.mouseClick(initialize_button, Qt.MouseButton.LeftButton) - def test_that_the_manage_experiments_tool_can_be_used_with_clean_storage( opened_main_window_poly, qtbot ): From fdc1d1017df4e1fa6fb638ebc8993c47d221315f Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 10:19:15 +0200 Subject: [PATCH 10/17] Remove redundant test --- .../test_multiple_data_assimilation_panel.py | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py b/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py index d465c72890d..ab679abf383 100644 --- a/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py +++ b/tests/ert/unit_tests/gui/experiments/test_multiple_data_assimilation_panel.py @@ -1,4 +1,3 @@ -import math from dataclasses import dataclass from unittest.mock import MagicMock @@ -248,40 +247,6 @@ def inspect_and_close_dialog() -> None: return captured_value -@pytest.mark.skip -@pytest.mark.parametrize( - ("active_realizations", "expected_threshold"), - [ - ([True], 1.0), - ([True, False, True, True, True, True, True, True], 1), - ( - [False, True, True] * 5, - 3 / math.sqrt(10), - ), - ([True, False] * 200, 3 / math.sqrt(200)), - ], -) -def test_that_analysis_module_edit_threshold_matches_expected_from_ensemble_size_via_ui( - qtbot: QtBot, active_realizations, expected_threshold -) -> None: - notifier = ErtNotifier() - notifier._storage = MockStorage() - - panel = MultipleDataAssimilationPanel( - analysis_config=AnalysisConfig(minimum_required_realizations=1), - parameter_configuration=EnsembleConfig().parameter_configuration, - run_path="", - notifier=notifier, - active_realizations=active_realizations, - config_num_realization=len(active_realizations), - ) - qtbot.addWidget(panel) - - observed_threshold = _open_and_capture_threshold(panel, qtbot) - - assert observed_threshold == pytest.approx(expected_threshold) - - @dataclass(frozen=True) class EnsInfo: ensemble_id: str From 499690e8585815edca75dd476286755031cf0eb0 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 10:45:57 +0200 Subject: [PATCH 11/17] Fix correlation threshold --- src/ert/config/analysis_module.py | 2 +- src/ert/gui/ertwidgets/analysismoduleedit.py | 10 +++++++--- tests/ert/unit_tests/config/test_analysis_module.py | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ert/config/analysis_module.py b/src/ert/config/analysis_module.py index fa09588abc7..5281966a4e0 100644 --- a/src/ert/config/analysis_module.py +++ b/src/ert/config/analysis_module.py @@ -86,7 +86,7 @@ def correlation_threshold(self, ensemble_size: int) -> float: Section 2.3 - Localization in the CHOP problem """ if self.localization_correlation_threshold is None: - return 3 / math.sqrt(ensemble_size) + return min(1.0, 3 / math.sqrt(ensemble_size)) return self.localization_correlation_threshold diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 3daeab24b65..cbba2945b5f 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -63,11 +63,15 @@ def _show_update_settings_dialog(self) -> None: parameter_config.update_strategy ) + correlation_threshold = 1.0 + if self._ensemble_size != 0: + correlation_threshold = self._es_settings.correlation_threshold( + self._ensemble_size + ) + update_settings_dialog = AnalysisModuleVariablesPanel( update_strategies=update_strategies, - correlation_threshold=self._es_settings.correlation_threshold( - self._ensemble_size - ), + correlation_threshold=correlation_threshold, enkf_truncation=self._es_settings.enkf_truncation, ) diff --git a/tests/ert/unit_tests/config/test_analysis_module.py b/tests/ert/unit_tests/config/test_analysis_module.py index 519cb7871ec..a352ee39b97 100644 --- a/tests/ert/unit_tests/config/test_analysis_module.py +++ b/tests/ert/unit_tests/config/test_analysis_module.py @@ -8,7 +8,8 @@ @pytest.mark.parametrize( ("settings", "ensemble_size", "expected_threshold"), [ - (ESSettings(), 8, 3 / math.sqrt(8)), + (ESSettings(), 4, 1.0), + (ESSettings(), 8, 1.0), (ESSettings(), 9, 1.0), (ESSettings(), 36, 0.5), (ESSettings(), 200, 3 / math.sqrt(200)), From 2988dcdd4cc2965f25f6255d5e0adaadd9909f2e Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Thu, 3 Sep 2026 10:52:04 +0200 Subject: [PATCH 12/17] Address review comments --- src/ert/gui/ertwidgets/analysismodulevariablespanel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py index 4f4e02c8c08..dc05f1c8c30 100644 --- a/src/ert/gui/ertwidgets/analysismodulevariablespanel.py +++ b/src/ert/gui/ertwidgets/analysismodulevariablespanel.py @@ -24,7 +24,7 @@ def __init__(self, exclude: set[LocalizationType] | None = None) -> None: if exclude is not None: type_set -= exclude - for localization_type in type_set: + for localization_type in sorted(type_set, key=lambda lt: lt.name): item = QStandardItem(localization_type.name) item.setData(localization_type, Qt.ItemDataRole.UserRole) self.appendRow(item) @@ -111,20 +111,20 @@ def __init__( var_name = "localization_correlation_threshold" metadata = AnalysisModule.model_fields[var_name] - self.treshold_spinner = self._create_double_spinbox( + self.threshold_spinner = self._create_double_spinbox( var_name, self._correlation_threshold, cast(float, next(v for v in metadata.metadata if isinstance(v, Ge)).ge), cast(float, next(v for v in metadata.metadata if isinstance(v, Le)).le), 0.1, ) - self.treshold_spinner.setObjectName("localization_correlation_threshold") - self.treshold_spinner.valueChanged.connect( + self.threshold_spinner.setObjectName("localization_correlation_threshold") + self.threshold_spinner.valueChanged.connect( lambda value: setattr(self, "_correlation_threshold", value) ) layout.addRow( - "Adaptive localization correlation threshold", self.treshold_spinner + "Adaptive localization correlation threshold", self.threshold_spinner ) self.setLayout(layout) From 95a41efeb0bd66aa3c5bfdfa404f823d57e37e43 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 7 Sep 2026 08:15:05 +0200 Subject: [PATCH 13/17] Address review comments --- src/ert/gui/ertwidgets/analysismoduleedit.py | 5 ++- .../gui/ertwidgets/test_analysismoduleedit.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index cbba2945b5f..964727ba0cc 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -105,5 +105,8 @@ def _show_update_settings_dialog(self) -> None: self._es_settings.enkf_truncation = update_settings_dialog.enkf_truncation for name, strategy in update_settings_dialog.update_strategies.items(): for parameter_config in self._parameter_config: - if parameter_config.type.upper() == name: + if ( + parameter_config.type.upper() == name + and parameter_config.update_strategy is not None + ): parameter_config.update_strategy = strategy diff --git a/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py b/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py index c00840b8978..7edfb54bb62 100644 --- a/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py +++ b/tests/ert/unit_tests/gui/ertwidgets/test_analysismoduleedit.py @@ -50,6 +50,7 @@ def test_that_settings_are_updated_correctly(qtbot: QtBot): qtbot.addWidget(widget) def inspect_and_accept_dialog() -> None: + dialog = QApplication.activeModalWidget() assert dialog is not None assert isinstance(dialog, QDialog) @@ -76,6 +77,44 @@ def inspect_and_accept_dialog() -> None: assert widget._parameter_config[0].update_strategy == LocalizationType.ADAPTIVE +def test_that_only_gen_kw_parameters_with_update_strategy_are_updated(qtbot: QtBot): + parameter_without_strategy = GenKwConfig( + name="without_strategy", + distribution={"name": "uniform", "min": 0, "max": 1}, + update_strategy=None, + ) + parameter_with_strategy = GenKwConfig( + name="with_strategy", + distribution={"name": "uniform", "min": 0, "max": 1}, + update_strategy=LocalizationType.GLOBAL, + ) + + widget = AnalysisModuleEdit( + es_settings=ESSettings(), + parameter_config=[parameter_without_strategy, parameter_with_strategy], + ensemble_size=10, + ) + qtbot.addWidget(widget) + + def select_adaptive_strategy_and_accept_dialog() -> None: + dialog = QApplication.activeModalWidget() + assert isinstance(dialog, QDialog) + + panel = dialog.findChild(AnalysisModuleVariablesPanel) + assert panel is not None + panel._update_strategies["GEN_KW"] = LocalizationType.ADAPTIVE + dialog.accept() + + QTimer.singleShot(0, select_adaptive_strategy_and_accept_dialog) + + button = widget.findChild(QPushButton) + assert button is not None + qtbot.mouseClick(button, Qt.MouseButton.LeftButton) + + assert parameter_without_strategy.update_strategy is None + assert parameter_with_strategy.update_strategy == LocalizationType.ADAPTIVE + + def test_that_settings_are_not_updated_on_cancel(qtbot: QtBot): es_settings = ESSettings() es_settings.localization_correlation_threshold = 0.5 From 8906a2a660acdc209a4a3a46907043ca0a90dcd1 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 7 Sep 2026 12:49:21 +0200 Subject: [PATCH 14/17] Fix manual update --- src/ert/gui/ertwidgets/analysismoduleedit.py | 8 +++++++ .../gui/experiments/manual_update_panel.py | 22 ++++++++++++++++--- src/ert/run_models/model_factory.py | 2 ++ src/ert/run_models/run_model_configs.py | 12 +++++++--- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/ert/gui/ertwidgets/analysismoduleedit.py b/src/ert/gui/ertwidgets/analysismoduleedit.py index 964727ba0cc..990eaee917c 100644 --- a/src/ert/gui/ertwidgets/analysismoduleedit.py +++ b/src/ert/gui/ertwidgets/analysismoduleedit.py @@ -44,6 +44,14 @@ def __init__( self.setLayout(layout) + @property + def parameter_config(self) -> list[ParameterConfig]: + return self._parameter_config + + @parameter_config.setter + def parameter_config(self, value: list[ParameterConfig]) -> None: + self._parameter_config = value + def _show_update_settings_dialog(self) -> None: dialog = QDialog(self.parent()) # type: ignore dialog.setWindowTitle("Update settings") diff --git a/src/ert/gui/experiments/manual_update_panel.py b/src/ert/gui/experiments/manual_update_panel.py index 9a674d8986d..af20c6d4e72 100644 --- a/src/ert/gui/experiments/manual_update_panel.py +++ b/src/ert/gui/experiments/manual_update_panel.py @@ -37,6 +37,7 @@ class Arguments: target_ensemble: str ensemble_size: int experiment_name: str + parameter_configuration: list[ParameterConfig] class ManualUpdatePanel(ExperimentConfigPanel): @@ -112,6 +113,9 @@ def show_only_with_response_data_filter( self._active_realizations_field.setObjectName("active_realizations_box") self._realizations_from_fs() layout.addRow("Active realizations", self._active_realizations_field) + self._active_realizations_field.getValidationSupport().validationChanged.connect( + self.experiment_configuration_changed + ) self._experiment_name_field = StringBox( TextModel(""), @@ -130,20 +134,31 @@ def show_only_with_response_data_filter( ) ) - self._active_realizations_field.getValidationSupport().validationChanged.connect( - self.experiment_configuration_changed - ) self._ensemble_selector.ensemble_populated.connect(self._realizations_from_fs) self._ensemble_selector.ensemble_populated.connect( self.experiment_configuration_changed ) + self._ensemble_selector.ensemble_populated.connect( + self._parameter_configuration_changed + ) + self._ensemble_selector.currentIndexChanged.connect(self._realizations_from_fs) + self._ensemble_selector.currentIndexChanged.connect( + self._parameter_configuration_changed + ) + self.setLayout(layout) @property def selected_update_method(self) -> str: return self._update_method_dropdown.currentText() + def _parameter_configuration_changed(self) -> None: + if self._ensemble_selector.selected_ensemble is not None: + self._analysis_module_edit.parameter_config = list( + self._ensemble_selector.selected_ensemble.experiment.parameter_configuration.values() + ) + @Slot(str) def _on_update_method_changed(self, new_method: str) -> None: if new_method == "ES Update": @@ -171,6 +186,7 @@ def get_experiment_arguments(self) -> Arguments: target_ensemble=self._ensemble_format_model.getValue(), # type: ignore ensemble_size=self._ensemble_size, experiment_name=self._experiment_name_field.get_text, + parameter_configuration=self._analysis_module_edit.parameter_config, ) def _realizations_from_fs(self) -> None: diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index 203c5999b30..fc2f518b6a2 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -319,6 +319,7 @@ def _setup_manual_update( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, + parameter_config=args.parameter_configuration, ) return ManualUpdate(**runmodel_config.model_dump(), status_queue=status_queue) @@ -355,6 +356,7 @@ def _setup_manual_update_enif( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, + parameter_config=[], ) return ManualUpdateEnIF(**runmodel_config.model_dump(), status_queue=status_queue) diff --git a/src/ert/run_models/run_model_configs.py b/src/ert/run_models/run_model_configs.py index e3807c20bef..fd5a7e5f887 100644 --- a/src/ert/run_models/run_model_configs.py +++ b/src/ert/run_models/run_model_configs.py @@ -304,6 +304,12 @@ class ManualUpdateConfig(UpdateRunModelConfig): ert_templates: list[tuple[str, str]] shape_registry: ShapeRegistry | None = None experiment_name: str + parameter_config: list[ + Annotated[ + (GenKwConfig | SurfaceConfig | FieldConfig | EverestControl), + Field(discriminator="type"), + ] + ] def to_experiment_config( self, *, prior_experiment_config: ExperimentConfig @@ -315,9 +321,9 @@ def to_experiment_config( "ensemble_id": self.ensemble_id, "ert_templates": self.ert_templates, **self._update_experiment_config(), - "parameter_configuration": prior_experiment_config.get( - "parameter_configuration", [] - ), + "parameter_configuration": [ + param.model_dump(mode="json") for param in self.parameter_config + ], "response_configuration": prior_experiment_config.get( "response_configuration", [] ), From 218c5724943b08cc907c9cc87d03531b886239fb Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 7 Sep 2026 13:09:52 +0200 Subject: [PATCH 15/17] Fix esmda --- .../multiple_data_assimilation_panel.py | 15 +++++++++++++++ src/ert/run_models/model_factory.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index 17fce30046b..69fb15929dd 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -66,6 +66,7 @@ class Arguments: weights: str prior_ensemble_id: str | None # UUID not serializable in json experiment_name: str + parameter_config: list[ParameterConfig] class MultipleDataAssimilationPanel(ExperimentConfigPanel): @@ -211,8 +212,15 @@ def get_ensembles_of_ensemble_experiment_type( layout.addRow("Select prior ensemble:", self._select_prior_ensemble_box) self._ensemble_selector.ensemble_populated.connect(self.select_prior_toggled) + self._ensemble_selector.ensemble_populated.connect( + self._parameter_configuration_changed + ) self._ensemble_selector.currentIndexChanged.connect(self._realizations_from_fs) + self._ensemble_selector.currentIndexChanged.connect( + self._parameter_configuration_changed + ) self._ensemble_selector.currentIndexChanged.connect(self.update_experiment_name) + layout.addRow("Run from prior ensemble:", self._ensemble_selector) self._experiment_name_field.getValidationSupport().validationChanged.connect( @@ -261,6 +269,12 @@ def _update_experiment_name_placeholder(self) -> None: self.notifier.storage.get_unique_experiment_name(ES_MDA_MODE) ) + def _parameter_configuration_changed(self) -> None: + if self._ensemble_selector.selected_ensemble is not None: + self._analysis_module_edit.parameter_config = list( + self._ensemble_selector.selected_ensemble.experiment.parameter_configuration.values() + ) + @Slot() def update_experiment_name(self) -> None: if not self._experiment_name_field.isEnabled(): @@ -433,6 +447,7 @@ def get_experiment_arguments(self) -> Arguments: else None ), experiment_name=self._experiment_name_field.get_text, + parameter_config=self._analysis_module_edit.parameter_config, ) def setWeights(self, weights: Any) -> None: diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index fc2f518b6a2..72d22623c21 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -490,7 +490,7 @@ def _setup_multiple_data_assimilation( parameter_configs, design_matrix = _merge_parameters( design_matrix=None if prior_ensemble else config.analysis_config.design_matrix, - parameter_configs=config.ensemble_config.parameter_configuration, + parameter_configs=args.parameter_config, require_updateable_param=True, ) From 8cdb41a61a38fea97a6c612c2119fe4226eeee3d Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 7 Sep 2026 14:22:13 +0200 Subject: [PATCH 16/17] Fix tests --- .../multiple_data_assimilation_panel.py | 4 +- src/ert/run_models/model_factory.py | 6 +- src/ert/run_models/run_model_configs.py | 4 +- .../heat_equationconfig.ert/config.json | 61 +------- .../poly_examplepoly.ert/poly.json | 45 +----- .../snake_oilsnake_oil.ert/snake_oil.json | 143 +----------------- .../heat_equationconfig.ert/config.json | 3 +- .../poly_examplepoly.ert/poly.json | 3 +- .../snake_oilsnake_oil.ert/snake_oil.json | 3 +- .../test_experiment_serialization.py | 2 + .../run_models/test_model_factory.py | 4 + 11 files changed, 22 insertions(+), 256 deletions(-) diff --git a/src/ert/gui/experiments/multiple_data_assimilation_panel.py b/src/ert/gui/experiments/multiple_data_assimilation_panel.py index 69fb15929dd..537c787bb4f 100644 --- a/src/ert/gui/experiments/multiple_data_assimilation_panel.py +++ b/src/ert/gui/experiments/multiple_data_assimilation_panel.py @@ -66,7 +66,7 @@ class Arguments: weights: str prior_ensemble_id: str | None # UUID not serializable in json experiment_name: str - parameter_config: list[ParameterConfig] + parameter_configuration: list[ParameterConfig] class MultipleDataAssimilationPanel(ExperimentConfigPanel): @@ -447,7 +447,7 @@ def get_experiment_arguments(self) -> Arguments: else None ), experiment_name=self._experiment_name_field.get_text, - parameter_config=self._analysis_module_edit.parameter_config, + parameter_configuration=self._analysis_module_edit.parameter_config, ) def setWeights(self, weights: Any) -> None: diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index 72d22623c21..e3b56d00a10 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -319,7 +319,7 @@ def _setup_manual_update( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_config=args.parameter_configuration, + parameter_configuration=args.parameter_configuration, ) return ManualUpdate(**runmodel_config.model_dump(), status_queue=status_queue) @@ -356,7 +356,7 @@ def _setup_manual_update_enif( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_config=[], + parameter_configuration=[], ) return ManualUpdateEnIF(**runmodel_config.model_dump(), status_queue=status_queue) @@ -490,7 +490,7 @@ def _setup_multiple_data_assimilation( parameter_configs, design_matrix = _merge_parameters( design_matrix=None if prior_ensemble else config.analysis_config.design_matrix, - parameter_configs=args.parameter_config, + parameter_configs=args.parameter_configuration, require_updateable_param=True, ) diff --git a/src/ert/run_models/run_model_configs.py b/src/ert/run_models/run_model_configs.py index fd5a7e5f887..dfe0583216e 100644 --- a/src/ert/run_models/run_model_configs.py +++ b/src/ert/run_models/run_model_configs.py @@ -304,7 +304,7 @@ class ManualUpdateConfig(UpdateRunModelConfig): ert_templates: list[tuple[str, str]] shape_registry: ShapeRegistry | None = None experiment_name: str - parameter_config: list[ + parameter_configuration: list[ Annotated[ (GenKwConfig | SurfaceConfig | FieldConfig | EverestControl), Field(discriminator="type"), @@ -322,7 +322,7 @@ def to_experiment_config( "ert_templates": self.ert_templates, **self._update_experiment_config(), "parameter_configuration": [ - param.model_dump(mode="json") for param in self.parameter_config + param.model_dump(mode="json") for param in self.parameter_configuration ], "response_configuration": prior_experiment_config.get( "response_configuration", [] diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/heat_equationconfig.ert/config.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/heat_equationconfig.ert/config.json index c2a6328ff4a..c7ef3ff50ee 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/heat_equationconfig.ert/config.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/heat_equationconfig.ert/config.json @@ -200,66 +200,7 @@ }, "experiment_name": "es-mda", "design_matrix": null, - "parameter_configuration": [ - { - "type": "gen_kw", - "name": "t", - "forward_init": false, - "update_strategy": "adaptive", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.5, - "max": 1.5 - }, - "group": "INIT_TEMP_SCALE", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "x", - "forward_init": false, - "update_strategy": "adaptive", - "dimensionality": 1, - "distribution": { - "name": "normal", - "mean": 0.8, - "std": 0.1 - }, - "group": "CORR_LENGTH", - "input_source": "sampled" - }, - { - "type": "field", - "name": "COND", - "forward_init": true, - "update_strategy": "adaptive", - "dimensionality": 3, - "ertbox_params": { - "nx": 50, - "ny": 50, - "nz": 1, - "axis_orientation": 1, - "xlength": 50.0, - "ylength": 50.0, - "xinc": 1.0, - "yinc": 1.0, - "rotation_angle": 0.0, - "origin": [ - 0.0, - 0.0 - ] - }, - "file_format": "bgrdecl", - "output_transformation": "EXP", - "input_transformation": "LN", - "truncation_min": null, - "truncation_max": null, - "forward_init_file": "cond.bgrdecl", - "output_file": "cond.bgrdecl", - "grid_file": "test-data/ert/heat_equation.config.ert/CASE.EGRID" - } - ], + "parameter_configuration": [], "response_configuration": [ { "type": "summary", diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/poly_examplepoly.ert/poly.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/poly_examplepoly.ert/poly.json index 8c3c2753baa..2acbfb9acde 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/poly_examplepoly.ert/poly.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/poly_examplepoly.ert/poly.json @@ -190,50 +190,7 @@ }, "experiment_name": "es-mda", "design_matrix": null, - "parameter_configuration": [ - { - "type": "gen_kw", - "name": "a", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.0, - "max": 1.0 - }, - "group": "COEFFS", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "b", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.0, - "max": 2.0 - }, - "group": "COEFFS", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "c", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.0, - "max": 5.0 - }, - "group": "COEFFS", - "input_source": "sampled" - } - ], + "parameter_configuration": [], "response_configuration": [ { "type": "gen_data", diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json index 938f3a48458..633bb30c7c2 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_esmda_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json @@ -166,148 +166,7 @@ }, "experiment_name": "es-mda", "design_matrix": null, - "parameter_configuration": [ - { - "type": "gen_kw", - "name": "BPR_555_PERSISTENCE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.1, - "max": 0.5 - }, - "group": "SNAKE_OIL_PARAM_BPR", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "BPR_138_PERSISTENCE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.2, - "max": 0.7 - }, - "group": "SNAKE_OIL_PARAM_BPR", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP1_PERSISTENCE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.01, - "max": 0.4 - }, - "group": "SNAKE_OIL_PARAM_OP1", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP1_OCTAVES", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 3.0, - "max": 5.0 - }, - "group": "SNAKE_OIL_PARAM_OP1", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP1_DIVERGENCE_SCALE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.25, - "max": 1.25 - }, - "group": "SNAKE_OIL_PARAM_OP1", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP1_OFFSET", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": -0.1, - "max": 0.1 - }, - "group": "SNAKE_OIL_PARAM_OP1", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP2_PERSISTENCE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.1, - "max": 0.6 - }, - "group": "SNAKE_OIL_PARAM_OP2", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP2_OCTAVES", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 5.0, - "max": 12.0 - }, - "group": "SNAKE_OIL_PARAM_OP2", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP2_DIVERGENCE_SCALE", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": 0.5, - "max": 1.5 - }, - "group": "SNAKE_OIL_PARAM_OP2", - "input_source": "sampled" - }, - { - "type": "gen_kw", - "name": "OP2_OFFSET", - "forward_init": false, - "update_strategy": "global", - "dimensionality": 1, - "distribution": { - "name": "uniform", - "min": -0.2, - "max": 0.2 - }, - "group": "SNAKE_OIL_PARAM_OP2", - "input_source": "sampled" - } - ], + "parameter_configuration": [], "response_configuration": [ { "type": "summary", diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/heat_equationconfig.ert/config.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/heat_equationconfig.ert/config.json index 12245d079ad..b24affe7e36 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/heat_equationconfig.ert/config.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/heat_equationconfig.ert/config.json @@ -233,5 +233,6 @@ } } }, - "experiment_name": "my_experiment" + "experiment_name": "my_experiment", + "parameter_configuration": [] } diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/poly_examplepoly.ert/poly.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/poly_examplepoly.ert/poly.json index fd83ddb19da..21c7aeea901 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/poly_examplepoly.ert/poly.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/poly_examplepoly.ert/poly.json @@ -194,5 +194,6 @@ "shape_registry": { "shapes": {} }, - "experiment_name": "my_experiment" + "experiment_name": "my_experiment", + "parameter_configuration": [] } diff --git a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json index 35ba3f01148..93c23be4bef 100644 --- a/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json +++ b/tests/ert/unit_tests/run_models/snapshots/test_experiment_serialization/test_that_dumped_manual_update_matches_snapshot/snake_oilsnake_oil.ert/snake_oil.json @@ -187,5 +187,6 @@ "shape_registry": { "shapes": {} }, - "experiment_name": "my_experiment" + "experiment_name": "my_experiment", + "parameter_configuration": [] } diff --git a/tests/ert/unit_tests/run_models/test_experiment_serialization.py b/tests/ert/unit_tests/run_models/test_experiment_serialization.py index 84cc4e777a1..be996dfd79e 100644 --- a/tests/ert/unit_tests/run_models/test_experiment_serialization.py +++ b/tests/ert/unit_tests/run_models/test_experiment_serialization.py @@ -748,6 +748,7 @@ def test_that_dumped_manual_update_matches_snapshot( ensemble_id=str(prior.id), target_ensemble="posterior", experiment_name="my_experiment", + parameter_configuration=[], ), case=f"{config_dir}.{config_file}", ) @@ -878,6 +879,7 @@ def test_that_dumped_esmda_matches_snapshot( weights="4, 2, 1", prior_ensemble_id=None, experiment_name="es-mda", + parameter_configuration=[], ), case=f"{config_dir}.{config_file}", ) diff --git a/tests/ert/unit_tests/run_models/test_model_factory.py b/tests/ert/unit_tests/run_models/test_model_factory.py index 1f774528230..46be412c8c5 100644 --- a/tests/ert/unit_tests/run_models/test_model_factory.py +++ b/tests/ert/unit_tests/run_models/test_model_factory.py @@ -72,6 +72,7 @@ def test_that_the_model_warns_when_active_realizations_less_min_realizations( num_iterations=1, prior_ensemble_id="", weights="2,3", + parameter_configuration=[], ), queue.SimpleQueue(), ) @@ -201,6 +202,7 @@ def test_that_setup_multiple_data_assimilation_uses_the_arguments_from_the_cli( prior_ensemble_id=None, experiment_name="My-experiment", starting_iteration=0, + parameter_configuration=[], ), ObservationSettings(), queue.SimpleQueue(), @@ -235,6 +237,7 @@ def test_that_setup_multiple_data_assimilation_uses_config_weights_when_cli_omit prior_ensemble_id=None, experiment_name="My-experiment", starting_iteration=0, + parameter_configuration=[], ), ObservationSettings(), queue.SimpleQueue(), @@ -279,6 +282,7 @@ def test_multiple_data_assimilation_restart_paths( target_ensemble="restart_case_%d", prior_ensemble_id=str(uuid1()), experiment_name="just_assimilatin", + parameter_configuration=[], ) monkeypatch.setattr( From 7b18d886f05b881b388a29e78b40102d1e8df8f2 Mon Sep 17 00:00:00 2001 From: Frode Aarstad Date: Mon, 7 Sep 2026 15:24:24 +0200 Subject: [PATCH 17/17] Small fix --- src/ert/run_models/model_factory.py | 14 +++++++++++--- tests/ert/ui_tests/cli/test_run_prior_esmda.py | 1 + .../unit_tests/run_models/test_manual_update.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ert/run_models/model_factory.py b/src/ert/run_models/model_factory.py index e3b56d00a10..96f09014207 100644 --- a/src/ert/run_models/model_factory.py +++ b/src/ert/run_models/model_factory.py @@ -319,7 +319,11 @@ def _setup_manual_update( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_configuration=args.parameter_configuration, + parameter_configuration=getattr( + args, + "parameter_configuration", + config.ensemble_config.parameter_configuration, + ), ) return ManualUpdate(**runmodel_config.model_dump(), status_queue=status_queue) @@ -356,7 +360,7 @@ def _setup_manual_update_enif( ert_templates=config.ert_templates, shape_registry=config.shape_registry, experiment_name=args.experiment_name, - parameter_configuration=[], + parameter_configuration=args.parameter_configuration, ) return ManualUpdateEnIF(**runmodel_config.model_dump(), status_queue=status_queue) @@ -490,7 +494,11 @@ def _setup_multiple_data_assimilation( parameter_configs, design_matrix = _merge_parameters( design_matrix=None if prior_ensemble else config.analysis_config.design_matrix, - parameter_configs=args.parameter_configuration, + parameter_configs=getattr( + args, + "parameter_configuration", + config.ensemble_config.parameter_configuration, + ), require_updateable_param=True, ) diff --git a/tests/ert/ui_tests/cli/test_run_prior_esmda.py b/tests/ert/ui_tests/cli/test_run_prior_esmda.py index 8e6ff70dbcd..1e708042e1b 100644 --- a/tests/ert/ui_tests/cli/test_run_prior_esmda.py +++ b/tests/ert/ui_tests/cli/test_run_prior_esmda.py @@ -227,6 +227,7 @@ def _build_esmda_run_prior_model(prior_ensemble_id: str): weights="1,1", prior_ensemble_id=prior_ensemble_id, experiment_name="restart-experiment", + parameter_configuration=[], ), SimpleQueue(), ) diff --git a/tests/ert/unit_tests/run_models/test_manual_update.py b/tests/ert/unit_tests/run_models/test_manual_update.py index 54f0cd95bb8..8385c8265ca 100644 --- a/tests/ert/unit_tests/run_models/test_manual_update.py +++ b/tests/ert/unit_tests/run_models/test_manual_update.py @@ -57,6 +57,7 @@ def test_that_manual_update_from_ensemble_experiment_supports_all_update_modes( ensemble_id=ensemble_id_to_update, target_ensemble="updated_ens%d", experiment_name="my manual update", + parameter_configuration=[], ), status_queue=queue.SimpleQueue(), )