ert 16.0.9__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 (286) hide show
  1. _ert/events.py +19 -2
  2. _ert/forward_model_runner/client.py +6 -2
  3. _ert/forward_model_runner/fm_dispatch.py +9 -6
  4. _ert/forward_model_runner/reporting/event.py +1 -0
  5. _ert/forward_model_runner/runner.py +1 -2
  6. _ert/utils.py +12 -0
  7. ert/__main__.py +58 -38
  8. ert/analysis/_enif_update.py +8 -4
  9. ert/analysis/_es_update.py +19 -6
  10. ert/analysis/_update_commons.py +16 -6
  11. ert/base_model_context.py +1 -1
  12. ert/cli/main.py +17 -12
  13. ert/cli/monitor.py +7 -0
  14. ert/config/__init__.py +17 -6
  15. ert/config/_create_observation_dataframes.py +118 -21
  16. ert/config/_get_num_cpu.py +1 -1
  17. ert/config/_observations.py +91 -2
  18. ert/config/_read_summary.py +74 -328
  19. ert/config/design_matrix.py +62 -23
  20. ert/config/distribution.py +1 -1
  21. ert/config/ensemble_config.py +9 -17
  22. ert/config/ert_config.py +155 -58
  23. ert/config/everest_control.py +234 -0
  24. ert/config/{everest_constraints_config.py → everest_response.py} +27 -15
  25. ert/config/field.py +99 -90
  26. ert/config/forward_model_step.py +122 -17
  27. ert/config/gen_data_config.py +5 -10
  28. ert/config/gen_kw_config.py +11 -41
  29. ert/config/known_response_types.py +14 -0
  30. ert/config/parameter_config.py +1 -33
  31. ert/config/parsing/_option_dict.py +10 -2
  32. ert/config/parsing/config_errors.py +1 -1
  33. ert/config/parsing/config_keywords.py +2 -1
  34. ert/config/parsing/config_schema.py +23 -11
  35. ert/config/parsing/config_schema_deprecations.py +3 -3
  36. ert/config/parsing/config_schema_item.py +26 -11
  37. ert/config/parsing/context_values.py +3 -3
  38. ert/config/parsing/file_context_token.py +1 -1
  39. ert/config/parsing/observations_parser.py +6 -2
  40. ert/config/parsing/queue_system.py +9 -0
  41. ert/config/parsing/schema_item_type.py +1 -0
  42. ert/config/queue_config.py +42 -50
  43. ert/config/response_config.py +0 -8
  44. ert/config/rft_config.py +275 -0
  45. ert/config/summary_config.py +3 -8
  46. ert/config/surface_config.py +73 -26
  47. ert/config/workflow_fixtures.py +2 -1
  48. ert/config/workflow_job.py +135 -54
  49. ert/dark_storage/client/__init__.py +2 -2
  50. ert/dark_storage/client/_session.py +4 -4
  51. ert/dark_storage/client/client.py +2 -2
  52. ert/dark_storage/common.py +12 -3
  53. ert/dark_storage/compute/misfits.py +11 -7
  54. ert/dark_storage/endpoints/compute/misfits.py +6 -4
  55. ert/dark_storage/endpoints/ensembles.py +4 -0
  56. ert/dark_storage/endpoints/experiment_server.py +30 -24
  57. ert/dark_storage/endpoints/experiments.py +2 -2
  58. ert/dark_storage/endpoints/observations.py +8 -6
  59. ert/dark_storage/endpoints/parameters.py +4 -12
  60. ert/dark_storage/endpoints/responses.py +24 -5
  61. ert/dark_storage/json_schema/ensemble.py +3 -0
  62. ert/dark_storage/json_schema/experiment.py +1 -1
  63. ert/data/_measured_data.py +6 -5
  64. ert/ensemble_evaluator/__init__.py +8 -1
  65. ert/ensemble_evaluator/config.py +2 -1
  66. ert/ensemble_evaluator/evaluator.py +81 -29
  67. ert/ensemble_evaluator/event.py +6 -0
  68. ert/ensemble_evaluator/snapshot.py +3 -1
  69. ert/ensemble_evaluator/state.py +1 -0
  70. ert/field_utils/__init__.py +8 -0
  71. ert/field_utils/field_utils.py +228 -15
  72. ert/field_utils/grdecl_io.py +1 -1
  73. ert/field_utils/roff_io.py +1 -1
  74. ert/gui/__init__.py +5 -2
  75. ert/gui/ertnotifier.py +1 -1
  76. ert/gui/ertwidgets/__init__.py +23 -16
  77. ert/gui/ertwidgets/analysismoduleedit.py +2 -2
  78. ert/gui/ertwidgets/checklist.py +1 -1
  79. ert/gui/ertwidgets/closabledialog.py +2 -0
  80. ert/gui/ertwidgets/copyablelabel.py +2 -0
  81. ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
  82. ert/gui/ertwidgets/ensembleselector.py +2 -2
  83. ert/gui/ertwidgets/listeditbox.py +2 -0
  84. ert/gui/ertwidgets/models/__init__.py +2 -0
  85. ert/gui/ertwidgets/models/activerealizationsmodel.py +5 -1
  86. ert/gui/ertwidgets/models/path_model.py +1 -1
  87. ert/gui/ertwidgets/models/targetensemblemodel.py +5 -1
  88. ert/gui/ertwidgets/models/text_model.py +4 -1
  89. ert/gui/ertwidgets/pathchooser.py +0 -3
  90. ert/gui/ertwidgets/searchbox.py +17 -4
  91. ert/gui/ertwidgets/stringbox.py +2 -0
  92. ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
  93. ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +63 -30
  94. ert/gui/main.py +41 -13
  95. ert/gui/main_window.py +3 -7
  96. ert/gui/model/fm_step_list.py +3 -0
  97. ert/gui/model/real_list.py +1 -0
  98. ert/gui/model/snapshot.py +1 -0
  99. ert/gui/simulation/combobox_with_description.py +3 -0
  100. ert/gui/simulation/ensemble_experiment_panel.py +8 -2
  101. ert/gui/simulation/ensemble_information_filter_panel.py +7 -2
  102. ert/gui/simulation/ensemble_smoother_panel.py +8 -2
  103. ert/gui/simulation/evaluate_ensemble_panel.py +17 -7
  104. ert/gui/simulation/experiment_panel.py +18 -6
  105. ert/gui/simulation/manual_update_panel.py +35 -10
  106. ert/gui/simulation/multiple_data_assimilation_panel.py +13 -9
  107. ert/gui/simulation/run_dialog.py +47 -20
  108. ert/gui/simulation/single_test_run_panel.py +6 -3
  109. ert/gui/simulation/view/progress_widget.py +2 -0
  110. ert/gui/simulation/view/realization.py +5 -1
  111. ert/gui/simulation/view/update.py +2 -0
  112. ert/gui/summarypanel.py +20 -1
  113. ert/gui/tools/event_viewer/panel.py +3 -4
  114. ert/gui/tools/event_viewer/tool.py +2 -0
  115. ert/gui/tools/load_results/load_results_panel.py +1 -1
  116. ert/gui/tools/load_results/load_results_tool.py +2 -0
  117. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  118. ert/gui/tools/manage_experiments/manage_experiments_panel.py +2 -0
  119. ert/gui/tools/manage_experiments/storage_info_widget.py +121 -16
  120. ert/gui/tools/manage_experiments/storage_widget.py +4 -3
  121. ert/gui/tools/plot/customize/color_chooser.py +5 -2
  122. ert/gui/tools/plot/customize/customize_plot_dialog.py +2 -0
  123. ert/gui/tools/plot/customize/default_customization_view.py +4 -0
  124. ert/gui/tools/plot/customize/limits_customization_view.py +3 -0
  125. ert/gui/tools/plot/customize/statistics_customization_view.py +3 -0
  126. ert/gui/tools/plot/customize/style_chooser.py +2 -0
  127. ert/gui/tools/plot/customize/style_customization_view.py +3 -0
  128. ert/gui/tools/plot/data_type_keys_widget.py +2 -0
  129. ert/gui/tools/plot/data_type_proxy_model.py +3 -0
  130. ert/gui/tools/plot/plot_api.py +50 -28
  131. ert/gui/tools/plot/plot_ensemble_selection_widget.py +17 -10
  132. ert/gui/tools/plot/plot_widget.py +15 -2
  133. ert/gui/tools/plot/plot_window.py +41 -19
  134. ert/gui/tools/plot/plottery/plot_config.py +2 -0
  135. ert/gui/tools/plot/plottery/plot_context.py +14 -0
  136. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  137. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  138. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  139. ert/gui/tools/plot/plottery/plots/ensemble.py +13 -5
  140. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  141. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  142. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  143. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  144. ert/gui/tools/plot/plottery/plots/statistics.py +62 -20
  145. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  146. ert/gui/tools/plot/widgets/clearable_line_edit.py +9 -0
  147. ert/gui/tools/plot/widgets/filter_popup.py +2 -0
  148. ert/gui/tools/plot/widgets/filterable_kw_list_model.py +3 -0
  149. ert/gui/tools/plugins/plugin.py +1 -1
  150. ert/gui/tools/plugins/plugins_tool.py +2 -0
  151. ert/gui/tools/plugins/process_job_dialog.py +3 -0
  152. ert/gui/tools/workflows/workflow_dialog.py +2 -0
  153. ert/gui/tools/workflows/workflows_tool.py +2 -0
  154. ert/libres_facade.py +5 -7
  155. ert/logging/__init__.py +4 -1
  156. ert/mode_definitions.py +2 -0
  157. ert/plugins/__init__.py +4 -6
  158. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  159. ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
  160. ert/plugins/hook_specifications/__init__.py +0 -10
  161. ert/plugins/hook_specifications/jobs.py +0 -9
  162. ert/plugins/plugin_manager.py +53 -124
  163. ert/resources/forward_models/run_reservoirsimulator.py +8 -4
  164. ert/resources/forward_models/template_render.py +10 -10
  165. ert/resources/shell_scripts/delete_directory.py +2 -2
  166. ert/run_models/__init__.py +24 -6
  167. ert/run_models/_create_run_path.py +133 -38
  168. ert/run_models/ensemble_experiment.py +10 -4
  169. ert/run_models/ensemble_information_filter.py +8 -1
  170. ert/run_models/ensemble_smoother.py +9 -3
  171. ert/run_models/evaluate_ensemble.py +8 -6
  172. ert/run_models/event.py +7 -3
  173. ert/run_models/everest_run_model.py +337 -113
  174. ert/run_models/initial_ensemble_run_model.py +25 -24
  175. ert/run_models/manual_update.py +6 -3
  176. ert/run_models/manual_update_enif.py +37 -0
  177. ert/run_models/model_factory.py +78 -18
  178. ert/run_models/multiple_data_assimilation.py +22 -11
  179. ert/run_models/run_model.py +72 -73
  180. ert/run_models/single_test_run.py +7 -4
  181. ert/run_models/update_run_model.py +4 -2
  182. ert/runpaths.py +5 -6
  183. ert/sample_prior.py +9 -4
  184. ert/scheduler/__init__.py +10 -5
  185. ert/scheduler/driver.py +40 -0
  186. ert/scheduler/event.py +3 -1
  187. ert/scheduler/job.py +23 -13
  188. ert/scheduler/lsf_driver.py +15 -5
  189. ert/scheduler/openpbs_driver.py +10 -4
  190. ert/scheduler/scheduler.py +5 -0
  191. ert/scheduler/slurm_driver.py +20 -5
  192. ert/services/__init__.py +2 -2
  193. ert/services/_base_service.py +37 -20
  194. ert/services/_storage_main.py +20 -18
  195. ert/services/ert_server.py +317 -0
  196. ert/shared/_doc_utils/__init__.py +4 -2
  197. ert/shared/_doc_utils/ert_jobs.py +1 -4
  198. ert/shared/net_utils.py +43 -18
  199. ert/shared/storage/connection.py +3 -3
  200. ert/shared/version.py +3 -3
  201. ert/storage/__init__.py +14 -1
  202. ert/storage/local_ensemble.py +44 -13
  203. ert/storage/local_experiment.py +54 -34
  204. ert/storage/local_storage.py +90 -58
  205. ert/storage/migration/to10.py +3 -2
  206. ert/storage/migration/to11.py +9 -10
  207. ert/storage/migration/to12.py +19 -20
  208. ert/storage/migration/to13.py +28 -27
  209. ert/storage/migration/to14.py +3 -3
  210. ert/storage/migration/to15.py +25 -0
  211. ert/storage/migration/to16.py +38 -0
  212. ert/storage/migration/to17.py +42 -0
  213. ert/storage/migration/to18.py +11 -0
  214. ert/storage/migration/to19.py +34 -0
  215. ert/storage/migration/to20.py +23 -0
  216. ert/storage/migration/to21.py +25 -0
  217. ert/storage/migration/to6.py +3 -2
  218. ert/storage/migration/to7.py +12 -13
  219. ert/storage/migration/to8.py +9 -11
  220. ert/storage/migration/to9.py +5 -4
  221. ert/storage/realization_storage_state.py +7 -7
  222. ert/substitutions.py +12 -28
  223. ert/validation/active_range.py +7 -7
  224. ert/validation/ensemble_realizations_argument.py +4 -2
  225. ert/validation/rangestring.py +16 -16
  226. ert/workflow_runner.py +6 -3
  227. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/METADATA +21 -15
  228. ert-19.0.0rc2.dist-info/RECORD +524 -0
  229. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/WHEEL +1 -1
  230. everest/api/everest_data_api.py +14 -1
  231. everest/assets/everest_logo.svg +406 -0
  232. everest/bin/config_branch_script.py +30 -14
  233. everest/bin/everconfigdump_script.py +2 -10
  234. everest/bin/everest_script.py +53 -33
  235. everest/bin/everlint_script.py +3 -5
  236. everest/bin/kill_script.py +7 -5
  237. everest/bin/main.py +11 -24
  238. everest/bin/monitor_script.py +64 -35
  239. everest/bin/utils.py +58 -43
  240. everest/bin/visualization_script.py +23 -13
  241. everest/config/__init__.py +4 -1
  242. everest/config/control_config.py +81 -6
  243. everest/config/control_variable_config.py +4 -3
  244. everest/config/everest_config.py +102 -79
  245. everest/config/forward_model_config.py +5 -3
  246. everest/config/install_data_config.py +7 -5
  247. everest/config/install_job_config.py +45 -3
  248. everest/config/install_template_config.py +3 -3
  249. everest/config/optimization_config.py +19 -6
  250. everest/config/output_constraint_config.py +8 -2
  251. everest/config/server_config.py +6 -55
  252. everest/config/simulator_config.py +62 -17
  253. everest/config/utils.py +25 -105
  254. everest/config/validation_utils.py +34 -15
  255. everest/config_file_loader.py +30 -21
  256. everest/detached/__init__.py +0 -6
  257. everest/detached/client.py +7 -52
  258. everest/detached/everserver.py +19 -45
  259. everest/everest_storage.py +24 -40
  260. everest/gui/everest_client.py +2 -3
  261. everest/gui/main_window.py +2 -2
  262. everest/optimizer/everest2ropt.py +68 -42
  263. everest/optimizer/opt_model_transforms.py +15 -20
  264. everest/optimizer/utils.py +0 -29
  265. everest/plugins/hook_specs.py +0 -24
  266. everest/strings.py +1 -6
  267. everest/util/__init__.py +3 -1
  268. ert/config/everest_objective_config.py +0 -95
  269. ert/config/ext_param_config.py +0 -107
  270. ert/gui/tools/export/__init__.py +0 -3
  271. ert/gui/tools/export/export_panel.py +0 -83
  272. ert/gui/tools/export/export_tool.py +0 -67
  273. ert/gui/tools/export/exporter.py +0 -36
  274. ert/plugins/hook_specifications/ecl_config.py +0 -29
  275. ert/services/storage_service.py +0 -127
  276. ert/summary_key_type.py +0 -234
  277. ert-16.0.9.dist-info/RECORD +0 -521
  278. everest/bin/everexport_script.py +0 -53
  279. everest/config/sampler_config.py +0 -103
  280. everest/simulator/__init__.py +0 -88
  281. everest/simulator/everest_to_ert.py +0 -252
  282. /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
  283. /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
  284. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/entry_points.txt +0 -0
  285. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/licenses/COPYING +0 -0
  286. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ import numpy as np
5
5
  from PyQt6.QtCore import Qt
6
6
  from PyQt6.QtCore import pyqtSlot as Slot
7
7
  from PyQt6.QtWidgets import QFormLayout, QLabel, QWidget
8
+ from typing_extensions import override
8
9
 
9
10
  from ert.config import ErrorInfo
10
11
  from ert.gui.ertnotifier import ErtNotifier
@@ -13,9 +14,9 @@ from ert.gui.ertwidgets import (
13
14
  CopyableLabel,
14
15
  EnsembleSelector,
15
16
  StringBox,
17
+ Suggestor,
16
18
  )
17
19
  from ert.gui.simulation.experiment_config_panel import ExperimentConfigPanel
18
- from ert.gui.suggestor import Suggestor
19
20
  from ert.mode_definitions import EVALUATE_ENSEMBLE_MODE
20
21
  from ert.run_models.evaluate_ensemble import EvaluateEnsemble
21
22
  from ert.storage import RealizationStorageState
@@ -32,9 +33,7 @@ class Arguments:
32
33
 
33
34
 
34
35
  class EvaluateEnsemblePanel(ExperimentConfigPanel):
35
- def __init__(
36
- self, ensemble_size: int, run_path: str, notifier: ErtNotifier
37
- ) -> None:
36
+ def __init__(self, run_path: str, notifier: ErtNotifier) -> None:
38
37
  self.notifier = notifier
39
38
  super().__init__(EvaluateEnsemble)
40
39
  self.setObjectName("Evaluate_parameters_panel")
@@ -49,8 +48,14 @@ class EvaluateEnsemblePanel(ExperimentConfigPanel):
49
48
  runpath_label = CopyableLabel(text=run_path)
50
49
  layout.addRow("Runpath:", runpath_label)
51
50
 
52
- number_of_realizations_label = QLabel(f"<b>{ensemble_size}</b>")
53
- layout.addRow(QLabel("Number of realizations:"), number_of_realizations_label)
51
+ ensemble_size = 0
52
+ if self._ensemble_selector.selected_ensemble:
53
+ ensemble_size = self._ensemble_selector.selected_ensemble.ensemble_size
54
+
55
+ self._number_of_realizations_label = QLabel(f"<b>{ensemble_size}</b>")
56
+ layout.addRow(
57
+ QLabel("Number of realizations:"), self._number_of_realizations_label
58
+ )
54
59
 
55
60
  self._active_realizations_field = StringBox(
56
61
  ActiveRealizationsModel(ensemble_size, show_default=False), # type: ignore
@@ -58,7 +63,6 @@ class EvaluateEnsemblePanel(ExperimentConfigPanel):
58
63
  )
59
64
  self._realizations_validator = EnsembleRealizationsArgument(
60
65
  lambda: self._ensemble_selector.selected_ensemble,
61
- max_value=ensemble_size,
62
66
  required_realization_storage_states=[
63
67
  RealizationStorageState.PARAMETERS_LOADED
64
68
  ],
@@ -78,12 +82,14 @@ class EvaluateEnsemblePanel(ExperimentConfigPanel):
78
82
  )
79
83
  self._ensemble_selector.currentIndexChanged.connect(self._realizations_from_fs)
80
84
 
85
+ @override
81
86
  def isConfigurationValid(self) -> bool:
82
87
  return (
83
88
  self._active_realizations_field.isValid()
84
89
  and self._ensemble_selector.currentIndex() != -1
85
90
  )
86
91
 
92
+ @override
87
93
  def get_experiment_arguments(self) -> Arguments:
88
94
  assert self._ensemble_selector.selected_ensemble is not None
89
95
  return Arguments(
@@ -106,6 +112,9 @@ class EvaluateEnsemblePanel(ExperimentConfigPanel):
106
112
  if not any(mask):
107
113
  mask = parameters
108
114
  self._active_realizations_field.model.setValueFromMask(mask) # type: ignore
115
+ self._number_of_realizations_label.setText(
116
+ f"<b>{ensemble.ensemble_size}</b>"
117
+ )
109
118
  except OSError as err:
110
119
  logger.error(str(err))
111
120
  Suggestor(
@@ -114,6 +123,7 @@ class EvaluateEnsemblePanel(ExperimentConfigPanel):
114
123
  parent=self,
115
124
  ).show()
116
125
 
126
+ @override
117
127
  @Slot(QWidget)
118
128
  def experimentTypeChanged(self, w: QWidget) -> None:
119
129
  if isinstance(w, EvaluateEnsemblePanel):
@@ -55,9 +55,15 @@ def create_md_table(kv: dict[str, str], output: str) -> str:
55
55
 
56
56
 
57
57
  def get_simulation_thread(
58
- model: Any, rerun_failed_realizations: bool = False, use_ipc_protocol: bool = False
58
+ model: Any,
59
+ rerun_failed_realizations: bool = False,
60
+ use_ipc_protocol: bool = False,
61
+ prioritize_private_ip_address: bool = False,
59
62
  ) -> ErtThread:
60
- evaluator_server_config = EvaluatorServerConfig(use_ipc_protocol=use_ipc_protocol)
63
+ evaluator_server_config = EvaluatorServerConfig(
64
+ use_ipc_protocol=use_ipc_protocol,
65
+ prioritize_private_ip_address=prioritize_private_ip_address,
66
+ )
61
67
 
62
68
  def run() -> None:
63
69
  model.api.start_simulations_thread(
@@ -158,7 +164,6 @@ class ExperimentPanel(QWidget):
158
164
  True,
159
165
  )
160
166
 
161
- ensemble_size = config.ensemble_size
162
167
  active_realizations = config.active_realizations
163
168
  config_num_realization = config.runpath_config.num_realizations
164
169
  self.addExperimentConfigPanel(
@@ -173,7 +178,7 @@ class ExperimentPanel(QWidget):
173
178
  True,
174
179
  )
175
180
  self.addExperimentConfigPanel(
176
- EvaluateEnsemblePanel(ensemble_size, run_path, notifier),
181
+ EvaluateEnsemblePanel(run_path, notifier),
177
182
  True,
178
183
  )
179
184
 
@@ -215,7 +220,7 @@ class ExperimentPanel(QWidget):
215
220
  experiment_type_valid,
216
221
  )
217
222
  self.addExperimentConfigPanel(
218
- ManualUpdatePanel(ensemble_size, run_path, notifier, analysis_config),
223
+ ManualUpdatePanel(run_path, notifier, analysis_config),
219
224
  experiment_type_valid,
220
225
  )
221
226
 
@@ -355,6 +360,10 @@ class ExperimentPanel(QWidget):
355
360
  return
356
361
  QApplication.restoreOverrideCursor()
357
362
 
363
+ self.configuration_summary.log_summary(
364
+ args.mode, model.get_number_of_active_realizations()
365
+ )
366
+
358
367
  self._dialog = RunDialog(
359
368
  f"Experiment - {self._config_file} {find_ert_info()}",
360
369
  model.api,
@@ -365,7 +374,9 @@ class ExperimentPanel(QWidget):
365
374
  run_path=Path(self.config.runpath_config.runpath_format_string),
366
375
  storage_path=self._notifier.storage.path,
367
376
  )
368
- self._dialog.set_queue_system_name(model.queue_system)
377
+ self._dialog.queue_system.setText(
378
+ f"Queue system:\n{model.queue_config.queue_system.formatted_name}"
379
+ )
369
380
  self.experiment_started.emit(self._dialog)
370
381
  self._simulation_done = False
371
382
  self.run_button.setEnabled(self._simulation_done)
@@ -376,6 +387,7 @@ class ExperimentPanel(QWidget):
376
387
  rerun_failed_realizations,
377
388
  use_ipc_protocol=self.config.queue_config.queue_system
378
389
  == QueueSystem.LOCAL,
390
+ prioritize_private_ip_address=self.config.prioritize_private_ip_address,
379
391
  )
380
392
  self._dialog.setup_event_monitoring(rerun_failed_realizations)
381
393
  simulation_thread.start()
@@ -3,9 +3,10 @@ from dataclasses import dataclass
3
3
  from typing import cast
4
4
 
5
5
  import numpy as np
6
- from PyQt6.QtCore import Qt
6
+ from PyQt6.QtCore import pyqtSignal
7
7
  from PyQt6.QtCore import pyqtSlot as Slot
8
- from PyQt6.QtWidgets import QFormLayout, QLabel, QWidget
8
+ from PyQt6.QtWidgets import QComboBox, QFormLayout, QLabel, QWidget
9
+ from typing_extensions import override
9
10
 
10
11
  from ert.config import AnalysisConfig, ErrorInfo
11
12
  from ert.gui.ertnotifier import ErtNotifier
@@ -15,11 +16,11 @@ from ert.gui.ertwidgets import (
15
16
  CopyableLabel,
16
17
  EnsembleSelector,
17
18
  StringBox,
19
+ Suggestor,
18
20
  TargetEnsembleModel,
19
21
  )
20
22
  from ert.gui.simulation.experiment_config_panel import ExperimentConfigPanel
21
- from ert.gui.suggestor import Suggestor
22
- from ert.mode_definitions import MANUAL_UPDATE_MODE
23
+ from ert.mode_definitions import MANUAL_ENIF_UPDATE_MODE, MANUAL_UPDATE_MODE
23
24
  from ert.run_models.manual_update import ManualUpdate
24
25
  from ert.storage import Ensemble, RealizationStorageState
25
26
  from ert.validation import EnsembleRealizationsArgument, ProperNameFormatArgument
@@ -37,9 +38,10 @@ class Arguments:
37
38
 
38
39
 
39
40
  class ManualUpdatePanel(ExperimentConfigPanel):
41
+ updateMethodChanged = pyqtSignal(str)
42
+
40
43
  def __init__(
41
44
  self,
42
- ensemble_size: int,
43
45
  run_path: str,
44
46
  notifier: ErtNotifier,
45
47
  analysis_config: AnalysisConfig,
@@ -49,9 +51,17 @@ class ManualUpdatePanel(ExperimentConfigPanel):
49
51
  self.setObjectName("Manual_update_panel")
50
52
 
51
53
  layout = QFormLayout()
52
- lab = QLabel(ManualUpdate.name())
53
- lab.setAlignment(Qt.AlignmentFlag.AlignLeft)
54
- layout.addRow(lab)
54
+ self._update_method_dropdown = QComboBox()
55
+ self._update_method_dropdown.addItems(
56
+ ["ES Update", "EnIF Update (Experimental)"]
57
+ )
58
+ self._update_method_dropdown.currentTextChanged.connect(
59
+ self._on_update_method_changed
60
+ )
61
+ self._update_method_dropdown.setObjectName("manual_update_method_dropdown")
62
+
63
+ layout.addRow("Update method:", self._update_method_dropdown)
64
+
55
65
  self._ensemble_selector = EnsembleSelector(
56
66
  notifier, show_only_with_response_data=True
57
67
  )
@@ -97,15 +107,30 @@ class ManualUpdatePanel(ExperimentConfigPanel):
97
107
  self._ensemble_selector.currentIndexChanged.connect(self._realizations_from_fs)
98
108
  self.setLayout(layout)
99
109
 
110
+ @property
111
+ def selected_update_method(self) -> str:
112
+ return self._update_method_dropdown.currentText()
113
+
114
+ @Slot(str)
115
+ def _on_update_method_changed(self, new_method: str) -> None:
116
+ if new_method == "ES Update":
117
+ self._analysis_module_edit.show()
118
+ else:
119
+ self._analysis_module_edit.hide()
120
+
121
+ @override
100
122
  def isConfigurationValid(self) -> bool:
101
123
  return (
102
124
  self._active_realizations_field.isValid()
103
125
  and self._ensemble_selector.currentIndex() != -1
104
126
  )
105
127
 
128
+ @override
106
129
  def get_experiment_arguments(self) -> Arguments:
107
130
  return Arguments(
108
- mode=MANUAL_UPDATE_MODE,
131
+ mode=MANUAL_UPDATE_MODE
132
+ if self.selected_update_method == "ES Update"
133
+ else MANUAL_ENIF_UPDATE_MODE,
109
134
  ensemble_id=str(
110
135
  cast(Ensemble, self._ensemble_selector.selected_ensemble).id
111
136
  ),
@@ -126,7 +151,6 @@ class ManualUpdatePanel(ExperimentConfigPanel):
126
151
  self._active_realizations_field.setValidator(
127
152
  EnsembleRealizationsArgument(
128
153
  lambda: ensemble,
129
- max_value=ensemble.ensemble_size,
130
154
  required_realization_storage_states=[
131
155
  RealizationStorageState.PARAMETERS_LOADED,
132
156
  RealizationStorageState.RESPONSES_LOADED,
@@ -148,6 +172,7 @@ class ManualUpdatePanel(ExperimentConfigPanel):
148
172
  parent=self,
149
173
  ).show()
150
174
 
175
+ @override
151
176
  @Slot(QWidget)
152
177
  def experimentTypeChanged(self, w: QWidget) -> None:
153
178
  if isinstance(w, ManualUpdatePanel):
@@ -8,6 +8,7 @@ from PyQt6.QtCore import Qt
8
8
  from PyQt6.QtCore import pyqtSlot as Slot
9
9
  from PyQt6.QtGui import QFont
10
10
  from PyQt6.QtWidgets import QCheckBox, QFormLayout, QHBoxLayout, QLabel, QWidget
11
+ from typing_extensions import override
11
12
 
12
13
  from ert.config import ErrorInfo, ParameterConfig
13
14
  from ert.gui.ertnotifier import ErtNotifier
@@ -17,14 +18,14 @@ from ert.gui.ertwidgets import (
17
18
  CopyableLabel,
18
19
  EnsembleSelector,
19
20
  StringBox,
21
+ Suggestor,
20
22
  TargetEnsembleModel,
21
23
  TextModel,
22
24
  ValueModel,
25
+ get_parameters_button,
23
26
  )
24
- from ert.gui.ertwidgets.parameterviewer import get_parameters_button
25
- from ert.gui.suggestor import Suggestor
26
27
  from ert.mode_definitions import ES_MDA_MODE
27
- from ert.run_models import MultipleDataAssimilation
28
+ from ert.run_models import MultipleDataAssimilation, MultipleDataAssimilationConfig
28
29
  from ert.storage.realization_storage_state import RealizationStorageState
29
30
  from ert.validation import (
30
31
  ExperimentValidation,
@@ -113,7 +114,7 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
113
114
  self._target_ensemble_format_field.setValidator(ProperNameFormatArgument())
114
115
  layout.addRow("Target ensemble format:", self._target_ensemble_format_field)
115
116
 
116
- self.weights = MultipleDataAssimilation.default_weights
117
+ self.weights = MultipleDataAssimilationConfig.default_weights
117
118
  self.weights_valid = True
118
119
  self._createInputForWeights(layout)
119
120
 
@@ -139,7 +140,6 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
139
140
  self._ensemble_selector = EnsembleSelector(notifier)
140
141
  self._previous_ensemble_realizations_validator = EnsembleRealizationsArgument(
141
142
  lambda: self._ensemble_selector.selected_ensemble,
142
- max_value=len(active_realizations),
143
143
  required_realization_storage_states=[
144
144
  RealizationStorageState.RESPONSES_LOADED
145
145
  ],
@@ -189,12 +189,14 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
189
189
  merged_parameters
190
190
  )
191
191
 
192
- layout.addRow("Parameters:", get_parameters_button(merged_parameters, self))
192
+ if merged_parameters:
193
+ layout.addRow("Parameters", get_parameters_button(merged_parameters, self))
193
194
 
194
195
  self.setLayout(layout)
195
196
 
196
197
  self.notifier.ertChanged.connect(self._update_experiment_name_placeholder)
197
198
 
199
+ @override
198
200
  @Slot(QWidget)
199
201
  def experimentTypeChanged(self, w: QWidget) -> None:
200
202
  if isinstance(w, MultipleDataAssimilationPanel):
@@ -215,7 +217,7 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
215
217
 
216
218
  self._relative_iteration_weights_box.setText(
217
219
  self._ensemble_selector.selected_ensemble.relative_weights
218
- or MultipleDataAssimilation.default_weights
220
+ or MultipleDataAssimilationConfig.default_weights
219
221
  )
220
222
  self._evaluate_weights_box_enabled()
221
223
 
@@ -250,9 +252,9 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
250
252
  self._ensemble_selector.selected_ensemble is not None
251
253
  and self._ensemble_selector.selected_ensemble.relative_weights
252
254
  )
253
- or MultipleDataAssimilation.default_weights
255
+ or MultipleDataAssimilationConfig.default_weights
254
256
  if self._restart_box.isChecked()
255
- else MultipleDataAssimilation.default_weights
257
+ else MultipleDataAssimilationConfig.default_weights
256
258
  )
257
259
  if self._restart_box.isChecked():
258
260
  self._active_realizations_field.setValidator(
@@ -307,6 +309,7 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
307
309
 
308
310
  updateVisualizationOfNormalizedWeights() # To normalize the default weights
309
311
 
312
+ @override
310
313
  def isConfigurationValid(self) -> bool:
311
314
  return (
312
315
  self._experiment_name_field.isValid()
@@ -316,6 +319,7 @@ class MultipleDataAssimilationPanel(ExperimentConfigPanel):
316
319
  and self.weights_valid
317
320
  )
318
321
 
322
+ @override
319
323
  def get_experiment_arguments(self) -> Arguments:
320
324
  return Arguments(
321
325
  mode=ES_MDA_MODE,
@@ -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
@@ -28,16 +29,20 @@ from PyQt6.QtWidgets import (
28
29
  QVBoxLayout,
29
30
  QWidget,
30
31
  )
32
+ from typing_extensions import override
31
33
 
32
- from ert.config import ErrorInfo, QueueSystem, WarningInfo
34
+ from _ert.events import EnsembleEvaluationWarning
35
+ from ert.config import ErrorInfo, WarningInfo
33
36
  from ert.ensemble_evaluator import (
34
37
  EndEvent,
35
38
  FullSnapshotEvent,
36
39
  SnapshotUpdateEvent,
40
+ StartEvent,
37
41
  WarningEvent,
38
42
  )
39
43
  from ert.ensemble_evaluator import identifiers as ids
40
44
  from ert.gui.ertnotifier import ErtNotifier
45
+ from ert.gui.ertwidgets import Suggestor
41
46
  from ert.gui.model.fm_step_list import FMStepListProxyModel
42
47
  from ert.gui.model.node import IterNode
43
48
  from ert.gui.model.real_list import RealListModel
@@ -48,7 +53,6 @@ from ert.gui.model.snapshot import (
48
53
  RealIens,
49
54
  SnapshotModel,
50
55
  )
51
- from ert.gui.suggestor import Suggestor
52
56
  from ert.gui.tools.file import FileDialog
53
57
  from ert.run_models import (
54
58
  RunModelAPI,
@@ -175,6 +179,7 @@ class FMStepOverview(QTableView):
175
179
  error_textedit.moveCursor(QTextCursor.MoveOperation.Start)
176
180
  error_dialog.exec()
177
181
 
182
+ @override
178
183
  def mouseMoveEvent(self, e: QMouseEvent | None) -> None:
179
184
  if e:
180
185
  index = self.indexAt(e.pos())
@@ -256,6 +261,7 @@ class RunDialog(QFrame):
256
261
  self._fm_step_label.setObjectName("fm_step_label")
257
262
  self._fm_step_overview = FMStepOverview(self._snapshot_model, self)
258
263
 
264
+ self._start_time: datetime | None = None
259
265
  self.running_time = QLabel("Running time:\n -")
260
266
  self.running_time.setMinimumWidth(150)
261
267
  self.queue_system = QLabel("")
@@ -266,6 +272,9 @@ class RunDialog(QFrame):
266
272
  self.kill_button = QPushButton("Terminate experiment")
267
273
  self.rerun_button = QPushButton("Rerun failed simulations")
268
274
  self.rerun_button.setEnabled(False)
275
+ self.show_warnings_button = QPushButton()
276
+ self.set_show_warning_button_to_initial_state()
277
+ self.show_warnings_button.clicked.connect(self.toggle_fail_message_box)
269
278
 
270
279
  size = 20
271
280
  spin_movie = QMovie("img:loading.gif")
@@ -309,6 +318,7 @@ class RunDialog(QFrame):
309
318
  button_layout = QVBoxLayout()
310
319
  button_layout.addWidget(self.kill_button)
311
320
  button_layout.addWidget(self.rerun_button)
321
+ button_layout.addWidget(self.show_warnings_button)
312
322
  footer_layout.addLayout(button_layout)
313
323
 
314
324
  footer_widget_container = QWidget()
@@ -478,6 +488,7 @@ class RunDialog(QFrame):
478
488
  if failed:
479
489
  self.update_total_progress(1.0, "Failed")
480
490
  self._progress_widget.set_all_failed()
491
+ self.show_warnings_button.setText("Show errors")
481
492
  else:
482
493
  self.update_total_progress(1.0, "Experiment completed.")
483
494
 
@@ -495,6 +506,8 @@ class RunDialog(QFrame):
495
506
  ),
496
507
  parent=self,
497
508
  )
509
+ self.show_warnings_button.setEnabled(True)
510
+ self.show_warnings_button.setToolTip("")
498
511
  self.fail_msg_box.show()
499
512
 
500
513
  if self.post_simulation_warnings:
@@ -503,11 +516,23 @@ class RunDialog(QFrame):
503
516
  f"{len(self.post_simulation_warnings)} PostSimulationWarnings"
504
517
  )
505
518
 
519
+ def set_show_warning_button_to_initial_state(self) -> None:
520
+ self.show_warnings_button.setEnabled(False)
521
+ self.show_warnings_button.setText("Show warnings")
522
+ self.show_warnings_button.setToolTip("No warnings to show")
523
+
524
+ def toggle_fail_message_box(self) -> None:
525
+ if self.fail_msg_box is None:
526
+ return
527
+ self.fail_msg_box.setVisible(not self.fail_msg_box.isVisible())
528
+
506
529
  @Slot()
507
530
  def _on_ticker(self) -> None:
508
- runtime = self._run_model_api.get_runtime()
509
- running_time = f"Running time: {humanize.precisedelta(runtime)}"
510
- 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}")
511
536
 
512
537
  maximum_memory_usage = self._snapshot_model.root.max_memory_usage
513
538
 
@@ -524,11 +549,16 @@ class RunDialog(QFrame):
524
549
  def _on_event(self, event: object) -> None:
525
550
  model = self._snapshot_model
526
551
  match event:
552
+ case StartEvent():
553
+ self._start_time = event.timestamp
527
554
  case EndEvent(failed=failed, msg=msg):
528
555
  self.simulation_done.emit(failed, msg)
529
556
  self._ticker.stop()
530
557
  case WarningEvent(msg=msg):
531
558
  self.post_simulation_warnings.append(msg)
559
+ case EnsembleEvaluationWarning(warning_message=msg):
560
+ self._show_warning(msg)
561
+
532
562
  case FullSnapshotEvent(
533
563
  status_count=status_count, realization_count=realization_count
534
564
  ):
@@ -634,25 +664,22 @@ class RunDialog(QFrame):
634
664
  self.post_simulation_warnings.clear()
635
665
  self._is_rerunning_failed_realizations = True
636
666
  self.rerun_failed_realizations_experiment.emit()
667
+ self.set_show_warning_button_to_initial_state()
637
668
 
638
- def set_queue_system_name(self, queue_system: QueueSystem) -> None:
639
- match queue_system:
640
- case QueueSystem.LSF:
641
- formatted_queue_system = "LSF"
642
- case QueueSystem.LOCAL:
643
- formatted_queue_system = "Local"
644
- case QueueSystem.TORQUE:
645
- formatted_queue_system = "Torque/OpenPBS"
646
- case QueueSystem.SLURM:
647
- formatted_queue_system = "Slurm"
648
- case default:
649
- assert_never(default)
650
- self.queue_system.setText(f"Queue system:\n{formatted_queue_system}")
651
-
669
+ @override
652
670
  def hideEvent(self, event: QHideEvent | None) -> None:
653
671
  for file_dialog in self.findChildren(FileDialog):
654
672
  file_dialog.close()
655
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
+
656
683
 
657
684
  # Cannot use a non-static method here as
658
685
  # it is called when the object is destroyed
@@ -2,14 +2,15 @@ from dataclasses import dataclass
2
2
 
3
3
  from PyQt6.QtCore import Qt
4
4
  from PyQt6.QtWidgets import QFormLayout, QLabel
5
+ from typing_extensions import override
5
6
 
7
+ from ert.config import AnalysisConfig, ParameterConfig
6
8
  from ert.gui.ertnotifier import ErtNotifier
7
9
  from ert.gui.ertwidgets import CopyableLabel
8
10
  from ert.mode_definitions import TEST_RUN_MODE
9
11
  from ert.run_models import SingleTestRun
10
12
 
11
- from ...config import AnalysisConfig, ParameterConfig
12
- from ..ertwidgets.parameterviewer import get_parameters_button
13
+ from ..ertwidgets import get_parameters_button
13
14
  from ._design_matrix_panel import DesignMatrixPanel
14
15
  from .experiment_config_panel import ExperimentConfigPanel
15
16
 
@@ -54,9 +55,11 @@ class SingleTestRunPanel(ExperimentConfigPanel):
54
55
  merged_parameters
55
56
  )
56
57
 
57
- layout.addRow("Parameters:", get_parameters_button(merged_parameters, self))
58
+ if merged_parameters:
59
+ layout.addRow("Parameters", get_parameters_button(merged_parameters, self))
58
60
 
59
61
  self.setLayout(layout)
60
62
 
63
+ @override
61
64
  def get_experiment_arguments(self) -> Arguments:
62
65
  return Arguments(TEST_RUN_MODE, "ensemble", "single_test_run")
@@ -8,6 +8,7 @@ from PyQt6.QtWidgets import (
8
8
  QProgressBar,
9
9
  QVBoxLayout,
10
10
  )
11
+ from typing_extensions import override
11
12
 
12
13
  from ert.ensemble_evaluator.state import ENSEMBLE_STATE_FAILED, REAL_STATE_TO_COLOR
13
14
 
@@ -107,5 +108,6 @@ class ProgressWidget(QFrame):
107
108
  self.stop_waiting_progress_bar()
108
109
  self.repaint_components()
109
110
 
111
+ @override
110
112
  def resizeEvent(self, a0: QResizeEvent | None) -> None:
111
113
  self.repaint_components()
@@ -22,6 +22,7 @@ from PyQt6.QtWidgets import (
22
22
  QVBoxLayout,
23
23
  QWidget,
24
24
  )
25
+ from typing_extensions import override
25
26
 
26
27
  from ert.gui.model.real_list import RealListModel
27
28
  from ert.gui.model.snapshot import (
@@ -105,6 +106,7 @@ class RealizationDelegate(QStyledItemDelegate):
105
106
  self._color_lightgray = QColor("LightGray").lighter(120)
106
107
  self._pen_black = QPen(self._color_black, 2, Qt.PenStyle.SolidLine)
107
108
 
109
+ @override
108
110
  def paint(
109
111
  self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex
110
112
  ) -> None:
@@ -158,10 +160,12 @@ class RealizationDelegate(QStyledItemDelegate):
158
160
 
159
161
  painter.restore()
160
162
 
163
+ @override
161
164
  def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
162
165
  return self._size
163
166
 
164
- def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool: # noqa: A002
167
+ @override
168
+ def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool:
165
169
  if isinstance(event, QHelpEvent) and event.type() == QEvent.Type.ToolTip:
166
170
  mouse_pos = event.pos() + self.adjustment_point_for_job_rect_margin
167
171
  parent: RealizationWidget = cast(RealizationWidget, self.parent())
@@ -25,6 +25,7 @@ from PyQt6.QtWidgets import (
25
25
  QVBoxLayout,
26
26
  QWidget,
27
27
  )
28
+ from typing_extensions import override
28
29
 
29
30
  from ert.analysis.event import DataSection
30
31
  from ert.ensemble_evaluator import state
@@ -56,6 +57,7 @@ class UpdateLogTable(QTableWidget):
56
57
  for j, val in enumerate(row):
57
58
  self.setItem(i, j, QTableWidgetItem(str(val)))
58
59
 
60
+ @override
59
61
  def keyPressEvent(self, e: QKeyEvent | None) -> None:
60
62
  if e is not None and e.matches(QKeySequence.StandardKey.Copy):
61
63
  stream = ""
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.