Skip to content

Commit 595b0de

Browse files
committed
Fix gui manual update not validating against storage
This commit makes the ManualUpdatePanel validate ensemble_size against what is in the storage instead of just using the ensemble_size/num_realizations in config. The commit also makes it so that ensembles without RealizationStorageState.RESPONSES_LOADED will not be available for selection in the EnsembleSelector. This will mostly be ensembles created by ManualUpdate anyways.
1 parent 676a43e commit 595b0de

7 files changed

Lines changed: 276 additions & 90 deletions

File tree

src/ert/gui/ertwidgets/ensembleselector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
update_ert: bool = True,
3131
show_only_undefined: bool = False,
3232
show_only_no_children: bool = False,
33+
show_only_with_response_data: bool = False,
3334
) -> None:
3435
super().__init__()
3536
self.notifier = notifier
@@ -44,6 +45,7 @@ def __init__(
4445
# if the ensemble has not been used in an update, as that would
4546
# invalidate the result
4647
self._show_only_no_children = show_only_no_children
48+
self._show_only_with_response_data = show_only_with_response_data
4749
self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
4850

4951
self.setEnabled(False)
@@ -123,6 +125,10 @@ def _ensemble_list(self) -> Iterable[Ensemble]:
123125
ens.parent for ens in self.notifier.storage.ensembles if ens.parent
124126
]
125127
ensemble_list = [val for val in ensemble_list if val.id not in parents]
128+
if self._show_only_with_response_data:
129+
ensemble_list = [
130+
ensemble for ensemble in ensemble_list if ensemble.has_data()
131+
]
126132
return self.sort_ensembles(ensemble_list)
127133

128134
@classmethod

src/ert/gui/simulation/manual_update_panel.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Arguments:
3333
realizations: str
3434
ensemble_id: str
3535
target_ensemble: str
36+
ensemble_size: int
3637

3738

3839
class ManualUpdatePanel(ExperimentConfigPanel):
@@ -51,13 +52,18 @@ def __init__(
5152
lab = QLabel(ManualUpdate.name())
5253
lab.setAlignment(Qt.AlignmentFlag.AlignLeft)
5354
layout.addRow(lab)
54-
self._ensemble_selector = EnsembleSelector(notifier)
55+
self._ensemble_selector = EnsembleSelector(
56+
notifier, show_only_with_response_data=True
57+
)
5558
layout.addRow("Ensemble:", self._ensemble_selector)
5659
runpath_label = CopyableLabel(text=run_path)
5760
layout.addRow("Runpath:", runpath_label)
5861

59-
number_of_realizations_label = QLabel(f"<b>{ensemble_size}</b>")
60-
layout.addRow(QLabel("Number of realizations:"), number_of_realizations_label)
62+
self._number_of_realizations_label = QLabel()
63+
layout.addRow(
64+
QLabel("Number of realizations:"),
65+
self._number_of_realizations_label,
66+
)
6167

6268
self._ensemble_format_model = TargetEnsembleModel(analysis_config, notifier)
6369
self._ensemble_format_field = StringBox(
@@ -68,25 +74,16 @@ def __init__(
6874
self._ensemble_format_field.setValidator(ProperNameFormatArgument())
6975
layout.addRow("Ensemble format:", self._ensemble_format_field)
7076

71-
self._analysis_module_edit = AnalysisModuleEdit(
72-
analysis_config.es_settings, ensemble_size
73-
)
77+
self._analysis_module_edit = AnalysisModuleEdit(analysis_config.es_settings, 0)
7478
self._analysis_module_edit.setObjectName("ensemble_smoother_edit")
79+
self._analysis_module_edit.setEnabled(False)
7580
layout.addRow("Analysis module:", self._analysis_module_edit)
76-
81+
self._active_realizations_model = ActiveRealizationsModel(0, show_default=False)
7782
self._active_realizations_field = StringBox(
78-
ActiveRealizationsModel(ensemble_size, show_default=False), # type: ignore
83+
self._active_realizations_model, # type: ignore
7984
continuous_update=True,
8085
)
81-
self._realizations_validator = EnsembleRealizationsArgument(
82-
lambda: self._ensemble_selector.selected_ensemble,
83-
max_value=ensemble_size,
84-
required_realization_storage_states=[
85-
RealizationStorageState.PARAMETERS_LOADED,
86-
RealizationStorageState.RESPONSES_LOADED,
87-
],
88-
)
89-
self._active_realizations_field.setValidator(self._realizations_validator)
86+
self._active_realizations_field.setObjectName("active_realizations_box")
9087
self._realizations_from_fs()
9188
layout.addRow("Active realizations", self._active_realizations_field)
9289

@@ -114,6 +111,7 @@ def get_experiment_arguments(self) -> Arguments:
114111
),
115112
realizations=self._active_realizations_field.text(),
116113
target_ensemble=self._ensemble_format_model.getValue(), # type: ignore
114+
ensemble_size=self._ensemble_size,
117115
)
118116

119117
def _realizations_from_fs(self) -> None:
@@ -124,7 +122,24 @@ def _realizations_from_fs(self) -> None:
124122
parameters = ensemble.get_realization_mask_with_parameters()
125123
responses = ensemble.get_realization_mask_with_responses()
126124
mask = np.logical_and(parameters, responses)
127-
self._active_realizations_field.model.setValueFromMask(mask) # type: ignore
125+
self._ensemble_size = ensemble.ensemble_size
126+
self._active_realizations_field.setValidator(
127+
EnsembleRealizationsArgument(
128+
lambda: ensemble,
129+
max_value=ensemble.ensemble_size,
130+
required_realization_storage_states=[
131+
RealizationStorageState.PARAMETERS_LOADED,
132+
RealizationStorageState.RESPONSES_LOADED,
133+
],
134+
)
135+
)
136+
self._active_realizations_model.ensemble_size = ensemble.ensemble_size
137+
self._active_realizations_model.setValueFromMask(mask)
138+
self._number_of_realizations_label.setText(
139+
f"<b>{ensemble.ensemble_size}</b>"
140+
)
141+
self._analysis_module_edit.ensemble_size = ensemble.ensemble_size
142+
self._analysis_module_edit.setEnabled(bool(ensemble.ensemble_size))
128143
except OSError as err:
129144
logger.error(str(err))
130145
Suggestor(

src/ert/run_models/model_factory.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,14 @@ def _setup_multiple_data_assimilation(
409409
)
410410

411411

412-
def _realizations(args: Namespace, ensemble_size: int) -> npt.NDArray[np.bool_]:
412+
def _realizations(
413+
args: Namespace, ensemble_size_from_config: int
414+
) -> npt.NDArray[np.bool_]:
415+
ensemble_size = (
416+
args.ensemble_size
417+
if hasattr(args, "ensemble_size") and args.ensemble_size is not None
418+
else ensemble_size_from_config
419+
)
413420
if not hasattr(args, "realizations") or args.realizations is None:
414421
return np.ones(ensemble_size, dtype=bool)
415422
return np.array(

tests/ert/ui_tests/gui/test_restart_esmda.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_active_realizations_esmda(opened_main_window_poly, qtbot):
6666
simulation_mode_combo.setCurrentText(MultipleDataAssimilation.display_name())
6767
es_mda_panel = gui.findChild(QWidget, name="ES_MDA_panel")
6868
assert es_mda_panel
69-
active_reals = gui.findChild(StringBox, "active_realizations_box")
69+
active_reals = es_mda_panel.findChild(StringBox, "active_realizations_box")
7070
assert active_reals.text() == "0-9"
7171

7272
restart_checkbox = es_mda_panel.findChild(QCheckBox, name="restart_checkbox_esmda")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from datetime import datetime
2+
from uuid import uuid4
3+
4+
from ert.storage.local_ensemble import LocalEnsemble
5+
from ert.storage.local_ensemble import _Index as _EnsembleIndex
6+
from ert.storage.local_experiment import LocalExperiment
7+
from ert.storage.local_experiment import _Index as _ExperimentIndex
8+
from ert.storage.local_storage import LocalStorage
9+
from ert.storage.realization_storage_state import RealizationStorageState
10+
11+
REALIZATION_FINISHED_SUCCESSFULLY = {
12+
RealizationStorageState.PARAMETERS_LOADED,
13+
RealizationStorageState.RESPONSES_LOADED,
14+
}
15+
REALIZATION_UNDEFINED = {RealizationStorageState.UNDEFINED}
16+
REALIZATION_ONLY_PARAMETERS = {RealizationStorageState.PARAMETERS_LOADED}
17+
REALIZATION_FAILED_DURING_EVALUATION = {
18+
RealizationStorageState.PARAMETERS_LOADED,
19+
RealizationStorageState.FAILURE_IN_CURRENT,
20+
}
21+
22+
23+
class MockEnsemble(LocalEnsemble):
24+
def __init__(self, ensemble_name, experiment_id, storage_states, storage) -> None:
25+
self._index = _EnsembleIndex(
26+
id=uuid4(),
27+
experiment_id=experiment_id,
28+
ensemble_size=len(storage_states),
29+
iteration=0,
30+
name=ensemble_name,
31+
prior_ensemble_id=None,
32+
started_at=datetime.now(),
33+
)
34+
self._storage_state = storage_states
35+
self._storage = storage
36+
37+
def get_ensemble_state(self):
38+
return self._storage_state
39+
40+
41+
class MockExperiment(LocalExperiment):
42+
def __init__(self, experiment_name) -> None:
43+
self._index = _ExperimentIndex(id=uuid4(), name=experiment_name)
44+
45+
@property
46+
def relative_weights(self) -> str:
47+
return "4, 2, 1"
48+
49+
50+
class MockStorage(LocalStorage):
51+
def __init__(self) -> None:
52+
self._ensembles = {}
53+
self._experiments = {}
54+
55+
def _setup_mocked_run(
56+
self, ensemble_name, experiment_name, ensemble_states
57+
) -> None:
58+
mock_experiment = MockExperiment(experiment_name)
59+
mock_ensemble2 = MockEnsemble(
60+
ensemble_name,
61+
experiment_id=mock_experiment.id,
62+
storage_states=ensemble_states,
63+
storage=self,
64+
)
65+
self._ensembles[mock_ensemble2.id] = mock_ensemble2
66+
self._experiments[mock_experiment.id] = mock_experiment
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from PyQt6.QtCore import Qt
2+
from pytestqt.qtbot import QtBot
3+
4+
from ert.config.analysis_config import AnalysisConfig
5+
from ert.gui.ertnotifier import ErtNotifier
6+
from ert.gui.ertwidgets.ensembleselector import EnsembleSelector
7+
from ert.gui.ertwidgets.stringbox import StringBox
8+
from ert.gui.simulation.manual_update_panel import ManualUpdatePanel
9+
10+
from .conftest import (
11+
REALIZATION_FINISHED_SUCCESSFULLY,
12+
REALIZATION_UNDEFINED,
13+
MockStorage,
14+
)
15+
16+
17+
def test_that_active_realizations_selector_validates_with_ensemble_size_from_prior(
18+
qtbot: QtBot,
19+
) -> None:
20+
"""This is a test that makes sure that the active realizations field is
21+
validated against the ensemble size from the prior ensemble, and not
22+
the ensemble size from config.
23+
"""
24+
notifier = ErtNotifier()
25+
notifier._storage = MockStorage()
26+
config_ensemble_size = 2
27+
notifier._storage._setup_mocked_run(
28+
"mock_ensemble0",
29+
"mock_experiment0",
30+
[
31+
REALIZATION_UNDEFINED,
32+
REALIZATION_FINISHED_SUCCESSFULLY,
33+
REALIZATION_FINISHED_SUCCESSFULLY,
34+
REALIZATION_FINISHED_SUCCESSFULLY,
35+
REALIZATION_UNDEFINED,
36+
REALIZATION_FINISHED_SUCCESSFULLY,
37+
REALIZATION_FINISHED_SUCCESSFULLY,
38+
REALIZATION_FINISHED_SUCCESSFULLY,
39+
],
40+
)
41+
notifier._storage._setup_mocked_run(
42+
"mock_ensemble1",
43+
"mock_experiment1",
44+
[
45+
REALIZATION_UNDEFINED,
46+
REALIZATION_UNDEFINED,
47+
REALIZATION_FINISHED_SUCCESSFULLY,
48+
REALIZATION_FINISHED_SUCCESSFULLY,
49+
REALIZATION_UNDEFINED,
50+
REALIZATION_FINISHED_SUCCESSFULLY,
51+
],
52+
)
53+
panel = ManualUpdatePanel(
54+
ensemble_size=config_ensemble_size,
55+
analysis_config=AnalysisConfig(minimum_required_realizations=1),
56+
run_path="",
57+
notifier=notifier,
58+
)
59+
qtbot.addWidget(panel)
60+
61+
prior_ensemble0_number_of_realizations_with_responses = 6
62+
prior_ensemble0_ensemble_size = 8
63+
64+
prior_ensemble1_number_of_realizations_with_responses = 3
65+
prior_ensemble1_ensemble_size = 6
66+
realization_selector = panel.findChild(StringBox, "active_realizations_box")
67+
assert realization_selector is not None
68+
ensemble_selector = panel.findChild(EnsembleSelector)
69+
assert ensemble_selector.isEnabled()
70+
71+
index = ensemble_selector.findText("mock_ensemble0", Qt.MatchFlag.MatchContains)
72+
ensemble_selector.setCurrentIndex(index)
73+
assert ensemble_selector.currentText() == "mock_experiment0 : mock_ensemble0"
74+
assert (
75+
panel._number_of_realizations_label.text()
76+
== f"<b>{prior_ensemble0_number_of_realizations_with_responses}</b>"
77+
)
78+
79+
# Only these realizations in prior have RealizationStorageState.RESPONSES_LOADED
80+
assert realization_selector.text() == "1-3, 5-7"
81+
assert realization_selector.isValid(), (
82+
realization_selector._validation._validation_message
83+
)
84+
assert panel.isConfigurationValid()
85+
assert (
86+
panel.get_experiment_arguments().ensemble_size == prior_ensemble0_ensemble_size
87+
)
88+
89+
# We try running a realization that does not have RESPONSES_LOADED
90+
realization_selector.setText("4-7")
91+
assert not panel.isConfigurationValid()
92+
index = ensemble_selector.findText("mock_ensemble1", Qt.MatchFlag.MatchContains)
93+
ensemble_selector.setCurrentIndex(index)
94+
assert ensemble_selector.currentText() == "mock_experiment1 : mock_ensemble1"
95+
96+
# The active realizations field should auto-populate with a valid value
97+
assert panel.isConfigurationValid()
98+
assert (
99+
panel.get_experiment_arguments().ensemble_size == prior_ensemble1_ensemble_size
100+
)
101+
assert (
102+
panel._number_of_realizations_label.text()
103+
== f"<b>{prior_ensemble1_number_of_realizations_with_responses}</b>"
104+
)
105+
assert realization_selector.text() == "2-3, 5"
106+
107+
108+
def test_that_manual_update_ensemble_selector_only_shows_ensembles_with_data(
109+
qtbot: QtBot,
110+
) -> None:
111+
"""This is a test that makes sure that ensembles without data are not available
112+
for selection in the ensemble_selector. This will be ensembles that were just
113+
created by ManageExperiment or by ManualUpdate.
114+
"""
115+
notifier = ErtNotifier()
116+
notifier._storage = MockStorage()
117+
config_ensemble_size = 2
118+
notifier._storage._setup_mocked_run(
119+
"mock_ensemble_no_data",
120+
"mock_experiment2",
121+
[REALIZATION_UNDEFINED, REALIZATION_UNDEFINED, REALIZATION_UNDEFINED],
122+
)
123+
124+
panel = ManualUpdatePanel(
125+
ensemble_size=config_ensemble_size,
126+
analysis_config=AnalysisConfig(minimum_required_realizations=1),
127+
run_path="",
128+
notifier=notifier,
129+
)
130+
qtbot.addWidget(panel)
131+
ensemble_selector = panel.findChild(EnsembleSelector)
132+
assert not ensemble_selector.isEnabled()
133+
134+
index = ensemble_selector.findText(
135+
"mock_ensemble_no_data", Qt.MatchFlag.MatchContains
136+
)
137+
assert index == -1 # Invalid index, because it is not available
138+
ensemble_selector.setCurrentIndex(index)
139+
assert not ensemble_selector.currentText()
140+
assert not panel.isConfigurationValid()

0 commit comments

Comments
 (0)