ert 17.0.0__py3-none-any.whl → 19.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. _ert/events.py +19 -2
  2. _ert/forward_model_runner/client.py +6 -2
  3. ert/__main__.py +28 -13
  4. ert/analysis/_enif_update.py +8 -4
  5. ert/analysis/_es_update.py +19 -6
  6. ert/analysis/_update_commons.py +16 -6
  7. ert/cli/main.py +13 -6
  8. ert/cli/monitor.py +7 -0
  9. ert/config/__init__.py +15 -6
  10. ert/config/_create_observation_dataframes.py +117 -20
  11. ert/config/_get_num_cpu.py +1 -1
  12. ert/config/_observations.py +91 -2
  13. ert/config/_read_summary.py +8 -6
  14. ert/config/design_matrix.py +51 -24
  15. ert/config/distribution.py +1 -1
  16. ert/config/ensemble_config.py +9 -17
  17. ert/config/ert_config.py +103 -19
  18. ert/config/everest_control.py +234 -0
  19. ert/config/{everest_objective_config.py → everest_response.py} +24 -15
  20. ert/config/field.py +96 -84
  21. ert/config/forward_model_step.py +122 -17
  22. ert/config/gen_data_config.py +5 -10
  23. ert/config/gen_kw_config.py +5 -35
  24. ert/config/known_response_types.py +14 -0
  25. ert/config/parameter_config.py +1 -33
  26. ert/config/parsing/_option_dict.py +10 -2
  27. ert/config/parsing/config_keywords.py +2 -0
  28. ert/config/parsing/config_schema.py +23 -3
  29. ert/config/parsing/config_schema_deprecations.py +3 -14
  30. ert/config/parsing/config_schema_item.py +26 -11
  31. ert/config/parsing/context_values.py +3 -3
  32. ert/config/parsing/file_context_token.py +1 -1
  33. ert/config/parsing/observations_parser.py +6 -2
  34. ert/config/parsing/queue_system.py +9 -0
  35. ert/config/parsing/schema_item_type.py +1 -0
  36. ert/config/queue_config.py +4 -5
  37. ert/config/response_config.py +0 -8
  38. ert/config/rft_config.py +275 -0
  39. ert/config/summary_config.py +3 -8
  40. ert/config/surface_config.py +59 -16
  41. ert/config/workflow_fixtures.py +2 -1
  42. ert/dark_storage/client/__init__.py +2 -2
  43. ert/dark_storage/client/_session.py +4 -4
  44. ert/dark_storage/client/client.py +2 -2
  45. ert/dark_storage/common.py +1 -1
  46. ert/dark_storage/compute/misfits.py +11 -7
  47. ert/dark_storage/endpoints/compute/misfits.py +6 -4
  48. ert/dark_storage/endpoints/experiment_server.py +12 -9
  49. ert/dark_storage/endpoints/experiments.py +2 -2
  50. ert/dark_storage/endpoints/observations.py +8 -6
  51. ert/dark_storage/endpoints/parameters.py +2 -18
  52. ert/dark_storage/endpoints/responses.py +24 -5
  53. ert/dark_storage/json_schema/experiment.py +1 -1
  54. ert/data/_measured_data.py +6 -5
  55. ert/ensemble_evaluator/__init__.py +8 -1
  56. ert/ensemble_evaluator/config.py +2 -1
  57. ert/ensemble_evaluator/evaluator.py +81 -29
  58. ert/ensemble_evaluator/event.py +6 -0
  59. ert/ensemble_evaluator/snapshot.py +3 -1
  60. ert/ensemble_evaluator/state.py +1 -0
  61. ert/field_utils/__init__.py +8 -0
  62. ert/field_utils/field_utils.py +212 -3
  63. ert/field_utils/roff_io.py +1 -1
  64. ert/gui/__init__.py +5 -2
  65. ert/gui/ertnotifier.py +1 -1
  66. ert/gui/ertwidgets/__init__.py +23 -16
  67. ert/gui/ertwidgets/analysismoduleedit.py +2 -2
  68. ert/gui/ertwidgets/checklist.py +1 -1
  69. ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
  70. ert/gui/ertwidgets/ensembleselector.py +2 -2
  71. ert/gui/ertwidgets/models/__init__.py +2 -0
  72. ert/gui/ertwidgets/models/activerealizationsmodel.py +2 -1
  73. ert/gui/ertwidgets/models/path_model.py +1 -1
  74. ert/gui/ertwidgets/models/targetensemblemodel.py +2 -1
  75. ert/gui/ertwidgets/models/text_model.py +1 -1
  76. ert/gui/ertwidgets/pathchooser.py +0 -3
  77. ert/gui/ertwidgets/searchbox.py +13 -4
  78. ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
  79. ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +63 -30
  80. ert/gui/main.py +37 -8
  81. ert/gui/main_window.py +1 -7
  82. ert/gui/simulation/ensemble_experiment_panel.py +1 -1
  83. ert/gui/simulation/ensemble_information_filter_panel.py +1 -1
  84. ert/gui/simulation/ensemble_smoother_panel.py +1 -1
  85. ert/gui/simulation/evaluate_ensemble_panel.py +1 -1
  86. ert/gui/simulation/experiment_panel.py +16 -3
  87. ert/gui/simulation/manual_update_panel.py +31 -8
  88. ert/gui/simulation/multiple_data_assimilation_panel.py +12 -8
  89. ert/gui/simulation/run_dialog.py +27 -20
  90. ert/gui/simulation/single_test_run_panel.py +2 -2
  91. ert/gui/summarypanel.py +20 -1
  92. ert/gui/tools/load_results/load_results_panel.py +1 -1
  93. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  94. ert/gui/tools/manage_experiments/storage_info_widget.py +121 -16
  95. ert/gui/tools/manage_experiments/storage_widget.py +1 -2
  96. ert/gui/tools/plot/plot_api.py +37 -25
  97. ert/gui/tools/plot/plot_widget.py +10 -2
  98. ert/gui/tools/plot/plot_window.py +38 -18
  99. ert/gui/tools/plot/plottery/plot_config.py +2 -0
  100. ert/gui/tools/plot/plottery/plot_context.py +14 -0
  101. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  102. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  103. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  104. ert/gui/tools/plot/plottery/plots/ensemble.py +12 -3
  105. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  106. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  107. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  108. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  109. ert/gui/tools/plot/plottery/plots/statistics.py +62 -20
  110. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  111. ert/mode_definitions.py +2 -0
  112. ert/plugins/__init__.py +0 -1
  113. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  114. ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
  115. ert/plugins/hook_specifications/__init__.py +0 -2
  116. ert/plugins/hook_specifications/jobs.py +0 -9
  117. ert/plugins/plugin_manager.py +6 -33
  118. ert/resources/forward_models/run_reservoirsimulator.py +8 -3
  119. ert/resources/shell_scripts/delete_directory.py +2 -2
  120. ert/run_models/__init__.py +18 -5
  121. ert/run_models/_create_run_path.py +131 -37
  122. ert/run_models/ensemble_experiment.py +10 -4
  123. ert/run_models/ensemble_information_filter.py +8 -1
  124. ert/run_models/ensemble_smoother.py +9 -3
  125. ert/run_models/evaluate_ensemble.py +8 -6
  126. ert/run_models/event.py +7 -3
  127. ert/run_models/everest_run_model.py +159 -46
  128. ert/run_models/initial_ensemble_run_model.py +25 -24
  129. ert/run_models/manual_update.py +6 -3
  130. ert/run_models/manual_update_enif.py +37 -0
  131. ert/run_models/model_factory.py +81 -21
  132. ert/run_models/multiple_data_assimilation.py +22 -11
  133. ert/run_models/run_model.py +64 -55
  134. ert/run_models/single_test_run.py +7 -4
  135. ert/run_models/update_run_model.py +4 -2
  136. ert/runpaths.py +5 -6
  137. ert/sample_prior.py +9 -4
  138. ert/scheduler/driver.py +37 -0
  139. ert/scheduler/event.py +3 -1
  140. ert/scheduler/job.py +23 -13
  141. ert/scheduler/lsf_driver.py +6 -2
  142. ert/scheduler/openpbs_driver.py +7 -1
  143. ert/scheduler/scheduler.py +5 -0
  144. ert/scheduler/slurm_driver.py +6 -2
  145. ert/services/__init__.py +2 -2
  146. ert/services/_base_service.py +37 -20
  147. ert/services/ert_server.py +317 -0
  148. ert/shared/_doc_utils/__init__.py +4 -2
  149. ert/shared/_doc_utils/ert_jobs.py +1 -4
  150. ert/shared/net_utils.py +43 -18
  151. ert/shared/storage/connection.py +3 -3
  152. ert/shared/version.py +3 -3
  153. ert/storage/__init__.py +2 -0
  154. ert/storage/local_ensemble.py +38 -12
  155. ert/storage/local_experiment.py +8 -16
  156. ert/storage/local_storage.py +68 -42
  157. ert/storage/migration/to11.py +1 -1
  158. ert/storage/migration/to16.py +38 -0
  159. ert/storage/migration/to17.py +42 -0
  160. ert/storage/migration/to18.py +11 -0
  161. ert/storage/migration/to19.py +34 -0
  162. ert/storage/migration/to20.py +23 -0
  163. ert/storage/migration/to21.py +25 -0
  164. ert/storage/migration/to8.py +4 -4
  165. ert/substitutions.py +12 -28
  166. ert/validation/active_range.py +7 -7
  167. ert/validation/rangestring.py +16 -16
  168. ert/workflow_runner.py +2 -1
  169. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/METADATA +9 -8
  170. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/RECORD +208 -205
  171. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/WHEEL +1 -1
  172. everest/api/everest_data_api.py +14 -1
  173. everest/bin/config_branch_script.py +3 -6
  174. everest/bin/everconfigdump_script.py +1 -9
  175. everest/bin/everest_script.py +21 -11
  176. everest/bin/everlint_script.py +0 -2
  177. everest/bin/kill_script.py +2 -2
  178. everest/bin/monitor_script.py +2 -2
  179. everest/bin/utils.py +8 -4
  180. everest/bin/visualization_script.py +6 -14
  181. everest/config/__init__.py +4 -1
  182. everest/config/control_config.py +81 -6
  183. everest/config/control_variable_config.py +4 -3
  184. everest/config/everest_config.py +75 -42
  185. everest/config/forward_model_config.py +5 -3
  186. everest/config/install_data_config.py +7 -5
  187. everest/config/install_job_config.py +7 -3
  188. everest/config/install_template_config.py +3 -3
  189. everest/config/optimization_config.py +19 -6
  190. everest/config/output_constraint_config.py +8 -2
  191. everest/config/server_config.py +6 -49
  192. everest/config/utils.py +25 -105
  193. everest/config/validation_utils.py +17 -11
  194. everest/config_file_loader.py +13 -4
  195. everest/detached/client.py +3 -3
  196. everest/detached/everserver.py +7 -8
  197. everest/everest_storage.py +6 -12
  198. everest/gui/everest_client.py +2 -3
  199. everest/gui/main_window.py +2 -2
  200. everest/optimizer/everest2ropt.py +59 -32
  201. everest/optimizer/opt_model_transforms.py +12 -13
  202. everest/optimizer/utils.py +0 -29
  203. everest/strings.py +0 -5
  204. ert/config/everest_constraints_config.py +0 -95
  205. ert/config/ext_param_config.py +0 -106
  206. ert/gui/tools/export/__init__.py +0 -3
  207. ert/gui/tools/export/export_panel.py +0 -83
  208. ert/gui/tools/export/export_tool.py +0 -69
  209. ert/gui/tools/export/exporter.py +0 -36
  210. ert/services/storage_service.py +0 -127
  211. everest/config/sampler_config.py +0 -103
  212. everest/simulator/__init__.py +0 -88
  213. everest/simulator/everest_to_ert.py +0 -51
  214. /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
  215. /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
  216. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/entry_points.txt +0 -0
  217. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/licenses/COPYING +0 -0
  218. {ert-17.0.0.dist-info → ert-19.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from datetime import datetime
4
5
  from pathlib import Path
5
6
  from queue import SimpleQueue
6
- from typing import assert_never, cast
7
+ from typing import cast
7
8
 
8
9
  import humanize
9
10
  from PyQt6.QtCore import QModelIndex, QSize, Qt, QThread, QTimer
@@ -30,15 +31,18 @@ from PyQt6.QtWidgets import (
30
31
  )
31
32
  from typing_extensions import override
32
33
 
33
- from ert.config import ErrorInfo, QueueSystem, WarningInfo
34
+ from _ert.events import EnsembleEvaluationWarning
35
+ from ert.config import ErrorInfo, WarningInfo
34
36
  from ert.ensemble_evaluator import (
35
37
  EndEvent,
36
38
  FullSnapshotEvent,
37
39
  SnapshotUpdateEvent,
40
+ StartEvent,
38
41
  WarningEvent,
39
42
  )
40
43
  from ert.ensemble_evaluator import identifiers as ids
41
44
  from ert.gui.ertnotifier import ErtNotifier
45
+ from ert.gui.ertwidgets import Suggestor
42
46
  from ert.gui.model.fm_step_list import FMStepListProxyModel
43
47
  from ert.gui.model.node import IterNode
44
48
  from ert.gui.model.real_list import RealListModel
@@ -49,7 +53,6 @@ from ert.gui.model.snapshot import (
49
53
  RealIens,
50
54
  SnapshotModel,
51
55
  )
52
- from ert.gui.suggestor import Suggestor
53
56
  from ert.gui.tools.file import FileDialog
54
57
  from ert.run_models import (
55
58
  RunModelAPI,
@@ -258,6 +261,7 @@ class RunDialog(QFrame):
258
261
  self._fm_step_label.setObjectName("fm_step_label")
259
262
  self._fm_step_overview = FMStepOverview(self._snapshot_model, self)
260
263
 
264
+ self._start_time: datetime | None = None
261
265
  self.running_time = QLabel("Running time:\n -")
262
266
  self.running_time.setMinimumWidth(150)
263
267
  self.queue_system = QLabel("")
@@ -524,9 +528,11 @@ class RunDialog(QFrame):
524
528
 
525
529
  @Slot()
526
530
  def _on_ticker(self) -> None:
527
- runtime = self._run_model_api.get_runtime()
528
- running_time = f"Running time: {humanize.precisedelta(runtime)}"
529
- self.running_time.setText(running_time[0:14] + "\n" + running_time[14:])
531
+ if self._start_time:
532
+ humanized_runtime = humanize.precisedelta(
533
+ datetime.now() - self._start_time, minimum_unit="seconds", format="%d"
534
+ )
535
+ self.running_time.setText(f"Running time:\n{humanized_runtime}")
530
536
 
531
537
  maximum_memory_usage = self._snapshot_model.root.max_memory_usage
532
538
 
@@ -543,11 +549,16 @@ class RunDialog(QFrame):
543
549
  def _on_event(self, event: object) -> None:
544
550
  model = self._snapshot_model
545
551
  match event:
552
+ case StartEvent():
553
+ self._start_time = event.timestamp
546
554
  case EndEvent(failed=failed, msg=msg):
547
555
  self.simulation_done.emit(failed, msg)
548
556
  self._ticker.stop()
549
557
  case WarningEvent(msg=msg):
550
558
  self.post_simulation_warnings.append(msg)
559
+ case EnsembleEvaluationWarning(warning_message=msg):
560
+ self._show_warning(msg)
561
+
551
562
  case FullSnapshotEvent(
552
563
  status_count=status_count, realization_count=realization_count
553
564
  ):
@@ -650,29 +661,25 @@ class RunDialog(QFrame):
650
661
  if result == QMessageBox.StandardButton.Ok:
651
662
  self.rerun_button.setEnabled(False)
652
663
  self.kill_button.setEnabled(True)
664
+ self.post_simulation_warnings.clear()
653
665
  self._is_rerunning_failed_realizations = True
654
666
  self.rerun_failed_realizations_experiment.emit()
655
667
  self.set_show_warning_button_to_initial_state()
656
668
 
657
- def set_queue_system_name(self, queue_system: QueueSystem) -> None:
658
- match queue_system:
659
- case QueueSystem.LSF:
660
- formatted_queue_system = "LSF"
661
- case QueueSystem.LOCAL:
662
- formatted_queue_system = "Local"
663
- case QueueSystem.TORQUE:
664
- formatted_queue_system = "Torque/OpenPBS"
665
- case QueueSystem.SLURM:
666
- formatted_queue_system = "Slurm"
667
- case default:
668
- assert_never(default)
669
- self.queue_system.setText(f"Queue system:\n{formatted_queue_system}")
670
-
671
669
  @override
672
670
  def hideEvent(self, event: QHideEvent | None) -> None:
673
671
  for file_dialog in self.findChildren(FileDialog):
674
672
  file_dialog.close()
675
673
 
674
+ def _show_warning(self, msg: str) -> None:
675
+ msg_box = QMessageBox(self)
676
+ msg_box.setObjectName("EnsembleEvaluationWarningBox")
677
+ msg_box.setIcon(QMessageBox.Icon.Warning)
678
+ msg_box.setWindowTitle("Ensemble Evaluation Warning")
679
+ msg_box.setText(msg)
680
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
681
+ msg_box.show()
682
+
676
683
 
677
684
  # Cannot use a non-static method here as
678
685
  # it is called when the object is destroyed
@@ -4,13 +4,13 @@ from PyQt6.QtCore import Qt
4
4
  from PyQt6.QtWidgets import QFormLayout, QLabel
5
5
  from typing_extensions import override
6
6
 
7
+ from ert.config import AnalysisConfig, ParameterConfig
7
8
  from ert.gui.ertnotifier import ErtNotifier
8
9
  from ert.gui.ertwidgets import CopyableLabel
9
10
  from ert.mode_definitions import TEST_RUN_MODE
10
11
  from ert.run_models import SingleTestRun
11
12
 
12
- from ...config import AnalysisConfig, ParameterConfig
13
- from ..ertwidgets.parameterviewer import get_parameters_button
13
+ from ..ertwidgets import get_parameters_button
14
14
  from ._design_matrix_panel import DesignMatrixPanel
15
15
  from .experiment_config_panel import ExperimentConfigPanel
16
16
 
ert/gui/summarypanel.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from PyQt6.QtCore import Qt
@@ -13,11 +14,13 @@ from PyQt6.QtWidgets import (
13
14
  QWidget,
14
15
  )
15
16
 
16
- from ert.gui.ertwidgets.models.ertsummary import ErtSummary
17
+ from ert.gui.ertwidgets import ErtSummary
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from ert.config import ErtConfig
20
21
 
22
+ logger = logging.getLogger(__name__)
23
+
21
24
 
22
25
  class SummaryTemplate:
23
26
  def __init__(self, title: str) -> None:
@@ -123,6 +126,22 @@ class SummaryPanel(QFrame):
123
126
 
124
127
  self._layout.addLayout(layout)
125
128
 
129
+ def log_summary(self, run_model: str, num_realizations: int) -> None:
130
+ summary = ErtSummary(self.config)
131
+
132
+ observations = summary.getObservations()
133
+ observations_count = sum(e["count"] for e in observations)
134
+
135
+ _, parameter_count = summary.get_parameters()
136
+
137
+ logger.info(
138
+ f"Experiment summary:\n"
139
+ f"Runmodel: {run_model}\n"
140
+ f"Realizations: {num_realizations}\n"
141
+ f"Parameters: {parameter_count}\n"
142
+ f"Observations: {observations_count}"
143
+ )
144
+
126
145
  @staticmethod
127
146
  def _runlength_encode_list(strings: list[str]) -> list[tuple[str, int]]:
128
147
  """Runlength encode a list of strings.
@@ -13,10 +13,10 @@ from ert.gui.ertwidgets import (
13
13
  EnsembleSelector,
14
14
  QApplication,
15
15
  StringBox,
16
+ Suggestor,
16
17
  TextBox,
17
18
  TextModel,
18
19
  )
19
- from ert.gui.suggestor import Suggestor
20
20
  from ert.run_models.run_model import captured_logs
21
21
  from ert.storage.local_ensemble import load_parameters_and_responses_from_runpath
22
22
  from ert.validation import RangeStringArgument, StringDefinition
@@ -0,0 +1,136 @@
1
+ import contextlib
2
+ from pathlib import Path
3
+ from typing import cast
4
+
5
+ import polars as pl
6
+ from PyQt6.QtCore import (
7
+ Qt,
8
+ )
9
+ from PyQt6.QtCore import (
10
+ pyqtSlot as Slot,
11
+ )
12
+ from PyQt6.QtGui import QColor, QPalette
13
+ from PyQt6.QtWidgets import (
14
+ QDialog,
15
+ QDialogButtonBox,
16
+ QFileDialog,
17
+ QHBoxLayout,
18
+ QLineEdit,
19
+ QPushButton,
20
+ QTextEdit,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+
26
+ class ExportDialog(QDialog):
27
+ """Base dialog for exporting ensemble-related data to files."""
28
+
29
+ def __init__(
30
+ self,
31
+ export_data: pl.DataFrame,
32
+ window_title: str = "Export data",
33
+ parent: QWidget | None = None,
34
+ ) -> None:
35
+ QDialog.__init__(self, parent)
36
+ self._export_data = export_data
37
+ self.setWindowTitle(window_title)
38
+
39
+ self.setWindowFlags(Qt.WindowType.Dialog)
40
+ self.setModal(True)
41
+ self.setMinimumWidth(450)
42
+ self.setMinimumHeight(200)
43
+
44
+ main_layout = QVBoxLayout()
45
+
46
+ file_layout = QHBoxLayout()
47
+ self._file_path_edit = QLineEdit(self)
48
+ self._file_path_edit.setPlaceholderText("Select output file...")
49
+ self._file_path_edit.textChanged.connect(self.validate_file)
50
+ browse_button = QPushButton("Browse...", self)
51
+ browse_button.clicked.connect(self.browse_file)
52
+ file_layout.addWidget(self._file_path_edit)
53
+ file_layout.addWidget(browse_button)
54
+ main_layout.addLayout(file_layout)
55
+
56
+ self._export_text_area = QTextEdit(self)
57
+ self._export_text_area.setReadOnly(True)
58
+ self._export_text_area.setFixedHeight(100)
59
+ main_layout.addWidget(self._export_text_area)
60
+
61
+ button_box = QDialogButtonBox(self)
62
+ button_box.setStandardButtons(QDialogButtonBox.StandardButton.Cancel)
63
+ button_box.rejected.connect(self.cancel)
64
+
65
+ self._export_button = cast(
66
+ QPushButton,
67
+ button_box.addButton("Export", QDialogButtonBox.ButtonRole.AcceptRole),
68
+ )
69
+ self._export_button.clicked.connect(self.export)
70
+ self._export_button.setEnabled(False) # Initially disabled
71
+ main_layout.addWidget(button_box)
72
+
73
+ self.setLayout(main_layout)
74
+
75
+ @Slot()
76
+ def export(self) -> None:
77
+ self._export_text_area.insertPlainText("Exporting...\n")
78
+ try:
79
+ output_file: str = self._file_path_edit.text().strip()
80
+ self._export_button.setEnabled(False)
81
+ self._export_data.write_csv(output_file, float_precision=6)
82
+ self._export_text_area.insertPlainText(f"Data exported to: {output_file}\n")
83
+ except Exception as e:
84
+ self._export_text_area.insertHtml(
85
+ f"<span style='color: red;'>Could not export data: {e!s}</span><br>"
86
+ )
87
+ finally:
88
+ self._export_button.setEnabled(True)
89
+
90
+ @Slot()
91
+ def cancel(self) -> None:
92
+ self.reject()
93
+
94
+ @Slot()
95
+ def validate_file(self) -> None:
96
+ """Validation to check if the file path is not empty or invalid."""
97
+
98
+ def _set_invalid(tooltip_text: str = "Invalid file path") -> None:
99
+ palette = self._file_path_edit.palette()
100
+ palette.setColor(QPalette.ColorRole.Text, QColor("red"))
101
+ self._file_path_edit.setPalette(palette)
102
+ self._file_path_edit.setToolTip(tooltip_text)
103
+ self._export_button.setToolTip(tooltip_text)
104
+ self._export_button.setEnabled(False)
105
+
106
+ def _set_valid() -> None:
107
+ palette = self._file_path_edit.palette()
108
+ palette.setColor(QPalette.ColorRole.Text, QColor("black"))
109
+ self._file_path_edit.setPalette(palette)
110
+ self._file_path_edit.setToolTip("")
111
+ self._export_button.setToolTip("")
112
+ self._export_button.setEnabled(True)
113
+
114
+ path = Path(self._file_path_edit.text().strip())
115
+ if str(path) in {"", "."}:
116
+ _set_invalid(tooltip_text="No filename provided")
117
+ return
118
+
119
+ if path.is_dir():
120
+ _set_invalid(tooltip_text=f"'{path!s}' is an existing directory.")
121
+ return
122
+
123
+ with contextlib.suppress(Exception):
124
+ if path.parent.is_dir():
125
+ _set_valid()
126
+ return
127
+
128
+ _set_invalid()
129
+
130
+ @Slot()
131
+ def browse_file(self) -> None:
132
+ file_path, _ = QFileDialog.getSaveFileName(
133
+ self, "Select Output File", "", "All Files (*)"
134
+ )
135
+ if file_path:
136
+ self._file_path_edit.setText(file_path)
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import json
2
3
  from enum import IntEnum
3
4
 
@@ -6,13 +7,18 @@ import seaborn as sns
6
7
  import yaml
7
8
  from matplotlib.backends.backend_qt5agg import FigureCanvas # type: ignore
8
9
  from matplotlib.figure import Figure
10
+ from polars import DataFrame
9
11
  from PyQt6.QtCore import Qt
10
12
  from PyQt6.QtCore import pyqtSlot as Slot
11
13
  from PyQt6.QtWidgets import (
14
+ QAbstractItemView,
12
15
  QFrame,
13
16
  QHBoxLayout,
14
17
  QLabel,
18
+ QPushButton,
15
19
  QStackedLayout,
20
+ QTableWidget,
21
+ QTableWidgetItem,
16
22
  QTabWidget,
17
23
  QTextEdit,
18
24
  QTreeWidget,
@@ -21,8 +27,11 @@ from PyQt6.QtWidgets import (
21
27
  QWidget,
22
28
  )
23
29
 
30
+ from ert import LibresFacade
24
31
  from ert.storage import Ensemble, Experiment, RealizationStorageState
25
32
 
33
+ from .export_dialog import ExportDialog
34
+
26
35
 
27
36
  class _WidgetType(IntEnum):
28
37
  EMPTY_WIDGET = 0
@@ -42,6 +51,8 @@ class _EnsembleWidgetTabs(IntEnum):
42
51
  ENSEMBLE_TAB = 0
43
52
  STATE_TAB = 1
44
53
  OBSERVATIONS_TAB = 2
54
+ PARAMETERS_TAB = 3
55
+ MISFIT_TAB = 4
45
56
 
46
57
 
47
58
  class _ExperimentWidget(QWidget):
@@ -126,12 +137,15 @@ class _EnsembleWidget(QWidget):
126
137
 
127
138
  info_frame.setLayout(info_layout)
128
139
 
140
+ state_frame = QFrame()
141
+ state_layout = QHBoxLayout()
129
142
  self._state_text_edit = QTextEdit()
130
143
  self._state_text_edit.setReadOnly(True)
131
144
  self._state_text_edit.setObjectName("ensemble_state_text")
145
+ state_layout.addWidget(self._state_text_edit)
146
+ state_frame.setLayout(state_layout)
132
147
 
133
148
  observations_frame = QFrame()
134
-
135
149
  self._observations_tree_widget = QTreeWidget(self)
136
150
  self._observations_tree_widget.currentItemChanged.connect(
137
151
  self._currentItemChanged
@@ -151,15 +165,38 @@ class _EnsembleWidget(QWidget):
151
165
  observations_layout.addWidget(self._canvas)
152
166
  observations_frame.setLayout(observations_layout)
153
167
 
168
+ self._parameters_table = QTableWidget()
169
+ self._parameters_table.setEditTriggers(
170
+ QAbstractItemView.EditTrigger.NoEditTriggers
171
+ )
172
+ self._export_params_button = QPushButton("Export...")
173
+ self._export_params_button.clicked.connect(self.onClickExportParameters)
174
+
175
+ parameters_frame = self.create_export_frame(
176
+ self._parameters_table, self._export_params_button
177
+ )
178
+
179
+ self._misfit_table = QTableWidget()
180
+ self._export_misfit_button = QPushButton("Export...")
181
+ self._export_misfit_button.clicked.connect(self.onClickExportMisfit)
182
+
183
+ misfit_frame = self.create_export_frame(
184
+ self._misfit_table, self._export_misfit_button
185
+ )
186
+
154
187
  self._tab_widget = QTabWidget()
155
188
  self._tab_widget.insertTab(
156
189
  _EnsembleWidgetTabs.ENSEMBLE_TAB, info_frame, "Ensemble"
157
190
  )
191
+ self._tab_widget.insertTab(_EnsembleWidgetTabs.STATE_TAB, state_frame, "State")
158
192
  self._tab_widget.insertTab(
159
- _EnsembleWidgetTabs.STATE_TAB, self._state_text_edit, "State"
193
+ _EnsembleWidgetTabs.OBSERVATIONS_TAB, observations_frame, "Observations"
160
194
  )
161
195
  self._tab_widget.insertTab(
162
- _EnsembleWidgetTabs.OBSERVATIONS_TAB, observations_frame, "Observations"
196
+ _EnsembleWidgetTabs.PARAMETERS_TAB, parameters_frame, "Parameters"
197
+ )
198
+ self._tab_widget.insertTab(
199
+ _EnsembleWidgetTabs.MISFIT_TAB, misfit_frame, "Misfit"
163
200
  )
164
201
  self._tab_widget.currentChanged.connect(self._currentTabChanged)
165
202
 
@@ -168,6 +205,17 @@ class _EnsembleWidget(QWidget):
168
205
 
169
206
  self.setLayout(layout)
170
207
 
208
+ def create_export_frame(self, table: QTableWidget, button: QPushButton) -> QFrame:
209
+ export_frame = QFrame()
210
+ export_layout = QVBoxLayout()
211
+ vertical_header = table.verticalHeader()
212
+ assert vertical_header is not None
213
+ vertical_header.setVisible(False)
214
+ export_layout.addWidget(table)
215
+ export_layout.addWidget(button)
216
+ export_frame.setLayout(export_layout)
217
+ return export_frame
218
+
171
219
  def _currentItemChanged(
172
220
  self, selected: QTreeWidgetItem, _: QTreeWidgetItem
173
221
  ) -> None:
@@ -189,9 +237,9 @@ class _EnsembleWidget(QWidget):
189
237
 
190
238
  response_type, obs_for_type = next(
191
239
  (
192
- (response_type, _df)
193
- for response_type, _df in observations_dict.items()
194
- if observation_key in _df["observation_key"]
240
+ (response_type, df)
241
+ for response_type, df in observations_dict.items()
242
+ if observation_key in df["observation_key"]
195
243
  ),
196
244
  (None, None),
197
245
  )
@@ -356,12 +404,53 @@ class _EnsembleWidget(QWidget):
356
404
  0, Qt.SortOrder.AscendingOrder
357
405
  )
358
406
 
359
- for i in range(self._observations_tree_widget.topLevelItemCount()):
360
- item = self._observations_tree_widget.topLevelItem(i)
361
- assert item is not None
362
- if item.childCount() > 0:
363
- self._observations_tree_widget.setCurrentItem(item.child(0))
364
- break
407
+ for i in range(self._observations_tree_widget.topLevelItemCount()):
408
+ item = self._observations_tree_widget.topLevelItem(i)
409
+ assert item is not None
410
+ if item.childCount() > 0:
411
+ self._observations_tree_widget.setCurrentItem(item.child(0))
412
+ break
413
+
414
+ elif index in {
415
+ _EnsembleWidgetTabs.PARAMETERS_TAB,
416
+ _EnsembleWidgetTabs.MISFIT_TAB,
417
+ }:
418
+ assert self._ensemble is not None
419
+
420
+ df: pl.DataFrame = pl.DataFrame()
421
+ with contextlib.suppress(Exception):
422
+ if index == _EnsembleWidgetTabs.PARAMETERS_TAB:
423
+ df = self._ensemble.load_scalar_keys(transformed=True)
424
+ else:
425
+ df = self.get_misfit_df()
426
+
427
+ table = (
428
+ self._parameters_table
429
+ if index == _EnsembleWidgetTabs.PARAMETERS_TAB
430
+ else self._misfit_table
431
+ )
432
+
433
+ table.setUpdatesEnabled(False)
434
+ table.setSortingEnabled(False)
435
+ table.setRowCount(df.height)
436
+ table.setColumnCount(df.width)
437
+ table.setHorizontalHeaderLabels(df.columns)
438
+
439
+ rows = df.rows()
440
+ for r, row in enumerate(rows):
441
+ for c, v in enumerate(row):
442
+ table.setItem(r, c, QTableWidgetItem("" if v is None else str(v)))
443
+
444
+ table.resizeColumnsToContents()
445
+ table.setUpdatesEnabled(True)
446
+
447
+ def get_misfit_df(self) -> DataFrame:
448
+ assert self._ensemble is not None
449
+ df = LibresFacade.load_all_misfit_data(self._ensemble)
450
+ realization_column = pl.Series(df.index)
451
+ df = pl.from_pandas(df)
452
+ df.insert_column(0, realization_column)
453
+ return df
365
454
 
366
455
  @Slot(Ensemble)
367
456
  def setEnsemble(self, ensemble: Ensemble) -> None:
@@ -372,6 +461,22 @@ class _EnsembleWidget(QWidget):
372
461
 
373
462
  self._tab_widget.setCurrentIndex(0)
374
463
 
464
+ @Slot()
465
+ def onClickExportMisfit(self) -> None:
466
+ assert self._ensemble is not None
467
+ misfit_df = self.get_misfit_df()
468
+ export_dialog = ExportDialog(misfit_df, "Export misfit", parent=self)
469
+ export_dialog.show()
470
+
471
+ @Slot()
472
+ def onClickExportParameters(self) -> None:
473
+ assert self._ensemble is not None
474
+ parameters_df = self._ensemble.load_scalar_keys(transformed=True)
475
+ export_dialog = ExportDialog(
476
+ parameters_df, window_title="Export parameters", parent=self
477
+ )
478
+ export_dialog.show()
479
+
375
480
 
376
481
  class _RealizationWidget(QWidget):
377
482
  def __init__(self) -> None:
@@ -418,14 +523,14 @@ class _RealizationWidget(QWidget):
418
523
  )
419
524
 
420
525
  html = "<table>"
421
- for name, _response_state in ensemble.get_response_state(realization).items():
422
- html += f"<tr><td>{name} - {_response_state.name}</td></tr>"
526
+ for name, response_state in ensemble.get_response_state(realization).items():
527
+ html += f"<tr><td>{name} - {response_state.name}</td></tr>"
423
528
  html += "</table>"
424
529
  self._response_text_edit.setHtml(html)
425
530
 
426
531
  html = "<table>"
427
- for name, _param_state in ensemble.get_parameter_state(realization).items():
428
- html += f"<tr><td>{name} - {_param_state.name}</td></tr>"
532
+ for name, param_state in ensemble.get_parameter_state(realization).items():
533
+ html += f"<tr><td>{name} - {param_state.name}</td></tr>"
429
534
  html += "</table>"
430
535
  self._parameter_text_edit.setHtml(html)
431
536
 
@@ -24,8 +24,7 @@ from PyQt6.QtWidgets import (
24
24
 
25
25
  from ert.config import ErrorInfo, ErtConfig
26
26
  from ert.gui.ertnotifier import ErtNotifier
27
- from ert.gui.ertwidgets.create_experiment_dialog import CreateExperimentDialog
28
- from ert.gui.suggestor import Suggestor
27
+ from ert.gui.ertwidgets import CreateExperimentDialog, Suggestor
29
28
  from ert.storage import Ensemble, Experiment
30
29
 
31
30
  from .storage_model import (