ert 18.0.9__py3-none-any.whl → 19.0.0__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 (116) hide show
  1. _ert/forward_model_runner/client.py +6 -2
  2. ert/__main__.py +20 -6
  3. ert/cli/main.py +7 -3
  4. ert/config/__init__.py +3 -4
  5. ert/config/_create_observation_dataframes.py +85 -59
  6. ert/config/_get_num_cpu.py +1 -1
  7. ert/config/_observations.py +106 -31
  8. ert/config/distribution.py +1 -1
  9. ert/config/ensemble_config.py +3 -3
  10. ert/config/ert_config.py +50 -0
  11. ert/config/{ext_param_config.py → everest_control.py} +8 -12
  12. ert/config/everest_response.py +3 -5
  13. ert/config/field.py +76 -14
  14. ert/config/forward_model_step.py +12 -9
  15. ert/config/gen_data_config.py +3 -4
  16. ert/config/gen_kw_config.py +2 -12
  17. ert/config/parameter_config.py +1 -16
  18. ert/config/parsing/_option_dict.py +10 -2
  19. ert/config/parsing/config_keywords.py +1 -0
  20. ert/config/parsing/config_schema.py +8 -0
  21. ert/config/parsing/config_schema_deprecations.py +3 -3
  22. ert/config/parsing/config_schema_item.py +12 -3
  23. ert/config/parsing/context_values.py +3 -3
  24. ert/config/parsing/file_context_token.py +1 -1
  25. ert/config/parsing/observations_parser.py +12 -2
  26. ert/config/parsing/queue_system.py +9 -0
  27. ert/config/queue_config.py +0 -1
  28. ert/config/response_config.py +0 -1
  29. ert/config/rft_config.py +78 -33
  30. ert/config/summary_config.py +1 -2
  31. ert/config/surface_config.py +59 -16
  32. ert/dark_storage/common.py +1 -1
  33. ert/dark_storage/compute/misfits.py +4 -1
  34. ert/dark_storage/endpoints/compute/misfits.py +4 -2
  35. ert/dark_storage/endpoints/experiment_server.py +12 -9
  36. ert/dark_storage/endpoints/experiments.py +2 -2
  37. ert/dark_storage/endpoints/observations.py +14 -4
  38. ert/dark_storage/endpoints/parameters.py +2 -18
  39. ert/dark_storage/endpoints/responses.py +10 -5
  40. ert/dark_storage/json_schema/experiment.py +1 -1
  41. ert/data/_measured_data.py +6 -5
  42. ert/ensemble_evaluator/config.py +2 -1
  43. ert/field_utils/field_utils.py +1 -1
  44. ert/field_utils/roff_io.py +1 -1
  45. ert/gui/__init__.py +5 -2
  46. ert/gui/ertnotifier.py +1 -1
  47. ert/gui/ertwidgets/pathchooser.py +0 -3
  48. ert/gui/ertwidgets/suggestor/suggestor.py +63 -30
  49. ert/gui/main.py +27 -5
  50. ert/gui/main_window.py +0 -5
  51. ert/gui/simulation/experiment_panel.py +12 -3
  52. ert/gui/simulation/run_dialog.py +2 -16
  53. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  54. ert/gui/tools/manage_experiments/storage_info_widget.py +133 -28
  55. ert/gui/tools/plot/plot_api.py +24 -15
  56. ert/gui/tools/plot/plot_widget.py +19 -4
  57. ert/gui/tools/plot/plot_window.py +35 -18
  58. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  59. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  60. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  61. ert/gui/tools/plot/plottery/plots/ensemble.py +3 -1
  62. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  63. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  64. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  65. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  66. ert/gui/tools/plot/plottery/plots/statistics.py +3 -1
  67. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  68. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  69. ert/plugins/plugin_manager.py +4 -0
  70. ert/resources/forward_models/run_reservoirsimulator.py +8 -3
  71. ert/run_models/_create_run_path.py +3 -3
  72. ert/run_models/everest_run_model.py +13 -11
  73. ert/run_models/initial_ensemble_run_model.py +2 -2
  74. ert/run_models/run_model.py +9 -0
  75. ert/services/_base_service.py +6 -5
  76. ert/services/ert_server.py +4 -4
  77. ert/shared/_doc_utils/__init__.py +4 -2
  78. ert/shared/net_utils.py +43 -18
  79. ert/shared/version.py +3 -3
  80. ert/storage/__init__.py +2 -0
  81. ert/storage/local_ensemble.py +25 -8
  82. ert/storage/local_experiment.py +2 -2
  83. ert/storage/local_storage.py +45 -25
  84. ert/storage/migration/to11.py +1 -1
  85. ert/storage/migration/to18.py +0 -1
  86. ert/storage/migration/to19.py +34 -0
  87. ert/storage/migration/to20.py +23 -0
  88. ert/storage/migration/to21.py +25 -0
  89. ert/storage/migration/to22.py +18 -0
  90. ert/storage/migration/to23.py +49 -0
  91. ert/workflow_runner.py +2 -1
  92. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/METADATA +1 -1
  93. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/RECORD +111 -109
  94. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/WHEEL +1 -1
  95. everest/bin/everlint_script.py +0 -2
  96. everest/bin/utils.py +2 -1
  97. everest/bin/visualization_script.py +4 -11
  98. everest/config/control_config.py +4 -4
  99. everest/config/control_variable_config.py +2 -2
  100. everest/config/everest_config.py +9 -0
  101. everest/config/utils.py +2 -2
  102. everest/config/validation_utils.py +7 -1
  103. everest/config_file_loader.py +0 -2
  104. everest/detached/client.py +3 -3
  105. everest/everest_storage.py +0 -2
  106. everest/gui/everest_client.py +2 -2
  107. everest/optimizer/everest2ropt.py +4 -4
  108. everest/optimizer/opt_model_transforms.py +2 -2
  109. ert/config/violations.py +0 -0
  110. ert/gui/tools/export/__init__.py +0 -3
  111. ert/gui/tools/export/export_panel.py +0 -83
  112. ert/gui/tools/export/export_tool.py +0 -69
  113. ert/gui/tools/export/exporter.py +0 -36
  114. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/entry_points.txt +0 -0
  115. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/licenses/COPYING +0 -0
  116. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/top_level.txt +0 -0
@@ -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:
@@ -175,9 +223,12 @@ class _EnsembleWidget(QWidget):
175
223
  return
176
224
 
177
225
  observation_key = selected.data(1, Qt.ItemDataRole.DisplayRole)
178
- if not observation_key:
226
+ parent = selected.parent()
227
+
228
+ if not observation_key or not parent:
179
229
  return
180
230
 
231
+ response_type = parent.data(0, Qt.ItemDataRole.UserRole)
181
232
  observation_label = selected.data(0, Qt.ItemDataRole.DisplayRole)
182
233
  assert self._ensemble is not None
183
234
  observations_dict = self._ensemble.experiment.observations
@@ -187,17 +238,7 @@ class _EnsembleWidget(QWidget):
187
238
  ax.set_title(observation_key)
188
239
  ax.grid(True)
189
240
 
190
- response_type, obs_for_type = next(
191
- (
192
- (response_type, df)
193
- for response_type, df in observations_dict.items()
194
- if observation_key in df["observation_key"]
195
- ),
196
- (None, None),
197
- )
198
-
199
- assert response_type is not None
200
- assert obs_for_type is not None
241
+ obs_for_type = observations_dict[response_type]
201
242
 
202
243
  response_config = self._ensemble.experiment.response_configuration[
203
244
  response_type
@@ -329,14 +370,23 @@ class _EnsembleWidget(QWidget):
329
370
  .to_numpy()
330
371
  ):
331
372
  match_list = self._observations_tree_widget.findItems(
332
- response_key, Qt.MatchFlag.MatchExactly
373
+ response_key, Qt.MatchFlag.MatchExactly, 0
333
374
  )
334
- if len(match_list) == 0:
375
+
376
+ root = next(
377
+ (
378
+ item
379
+ for item in match_list
380
+ if item.data(0, Qt.ItemDataRole.UserRole) == response_type
381
+ ),
382
+ None,
383
+ )
384
+
385
+ if root is None:
335
386
  root = QTreeWidgetItem(
336
387
  self._observations_tree_widget, [response_key]
337
388
  )
338
- else:
339
- root = match_list[0]
389
+ root.setData(0, Qt.ItemDataRole.UserRole, response_type)
340
390
 
341
391
  obs_ds = obs_ds_for_type.filter(
342
392
  pl.col("observation_key").eq(obs_key)
@@ -352,16 +402,55 @@ class _EnsembleWidget(QWidget):
352
402
  ],
353
403
  )
354
404
 
355
- self._observations_tree_widget.sortItems(
356
- 0, Qt.SortOrder.AscendingOrder
357
- )
405
+ self._observations_tree_widget.sortItems(0, Qt.SortOrder.AscendingOrder)
406
+
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)
358
438
 
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
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:
@@ -18,8 +18,9 @@ from pandas.api.types import is_numeric_dtype
18
18
  from pandas.errors import ParserError
19
19
  from resfo_utilities import history_key
20
20
 
21
- from ert.config import ParameterMetadata, ResponseMetadata
21
+ from ert.config import ParameterConfig, ResponseMetadata
22
22
  from ert.services import ErtServer
23
+ from ert.storage.local_experiment import _parameters_adapter as parameter_config_adapter
23
24
  from ert.storage.realization_storage_state import RealizationStorageState
24
25
 
25
26
  logger = logging.getLogger(__name__)
@@ -44,7 +45,7 @@ class PlotApiKeyDefinition(NamedTuple):
44
45
  dimensionality: int
45
46
  metadata: dict[Any, Any]
46
47
  filter_on: dict[Any, Any] | None = None
47
- parameter_metadata: ParameterMetadata | None = None
48
+ parameter: ParameterConfig | None = None
48
49
  response_metadata: ResponseMetadata | None = None
49
50
 
50
51
 
@@ -143,18 +144,21 @@ class PlotApi:
143
144
  self._check_response(response)
144
145
 
145
146
  for experiment in response.json():
146
- for param_metadatas in experiment["parameters"].values():
147
- for metadata in param_metadatas:
148
- param_key = metadata["key"]
149
- all_keys[param_key] = PlotApiKeyDefinition(
150
- key=param_key,
151
- index_type=None,
152
- observations=False,
153
- dimensionality=metadata["dimensionality"],
154
- metadata=metadata["userdata"],
155
- parameter_metadata=ParameterMetadata(**metadata),
156
- )
157
- all_params[param_key] = all_keys[param_key]
147
+ for metadata in experiment["parameters"].values():
148
+ param_cfg = parameter_config_adapter.validate_python(metadata)
149
+ if group := metadata.get("group"):
150
+ param_key = f"{group}:{metadata['name']}"
151
+ else:
152
+ param_key = metadata["name"]
153
+ all_keys[param_key] = PlotApiKeyDefinition(
154
+ key=param_key,
155
+ index_type=None,
156
+ observations=False,
157
+ dimensionality=metadata["dimensionality"],
158
+ metadata={"data_origin": metadata["type"]},
159
+ parameter=param_cfg,
160
+ )
161
+ all_params[param_key] = all_keys[param_key]
158
162
 
159
163
  return list(all_keys.values())
160
164
 
@@ -318,12 +322,17 @@ class PlotApi:
318
322
  f"ensemble_name={ensemble.name}, e={e}"
319
323
  ) from e
320
324
 
325
+ key_index: list[int | float | pd.Timestamp]
321
326
  for obs in observations:
322
327
  try:
323
328
  int(obs["x_axis"][0])
324
329
  key_index = [int(v) for v in obs["x_axis"]]
325
330
  except ValueError:
326
- key_index = [pd.Timestamp(v) for v in obs["x_axis"]]
331
+ try:
332
+ float(obs["x_axis"][0])
333
+ key_index = [float(v) for v in obs["x_axis"]]
334
+ except ValueError:
335
+ key_index = [pd.Timestamp(v) for v in obs["x_axis"]]
327
336
 
328
337
  observations_dfs.append(
329
338
  pd.DataFrame(
@@ -25,7 +25,7 @@ from PyQt6.QtWidgets import (
25
25
  )
26
26
  from typing_extensions import override
27
27
 
28
- from .plot_api import EnsembleObject
28
+ from .plot_api import EnsembleObject, PlotApiKeyDefinition
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from .plottery import PlotContext
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
34
34
  from .plottery.plots.ensemble import EnsemblePlot
35
35
  from .plottery.plots.gaussian_kde import GaussianKDEPlot
36
36
  from .plottery.plots.histogram import HistogramPlot
37
+ from .plottery.plots.misfits import MisfitsPlot
37
38
  from .plottery.plots.statistics import StatisticsPlot
38
39
  from .plottery.plots.std_dev import StdDevPlot
39
40
 
@@ -122,6 +123,7 @@ class PlotWidget(QWidget):
122
123
  "DistributionPlot",
123
124
  "CrossEnsembleStatisticsPlot",
124
125
  "StdDevPlot",
126
+ "MisfitsPlot",
125
127
  ],
126
128
  parent: QWidget | None = None,
127
129
  ) -> None:
@@ -161,6 +163,7 @@ class PlotWidget(QWidget):
161
163
  vbox.addSpacing(8)
162
164
  self.setLayout(vbox)
163
165
 
166
+ self._negative_values_in_data = False
164
167
  self._dirty = True
165
168
  self._active = False
166
169
  self.resetPlot()
@@ -173,7 +176,15 @@ class PlotWidget(QWidget):
173
176
  self._figure.clear()
174
177
 
175
178
  def _sync_log_checkbox(self) -> None:
176
- if type(self._plotter).__name__ == "HistogramPlot":
179
+ if (
180
+ type(self._plotter).__name__
181
+ in {
182
+ "HistogramPlot",
183
+ "DistributionPlot",
184
+ "GaussianKDEPlot",
185
+ }
186
+ and self._negative_values_in_data is False
187
+ ):
177
188
  self._log_checkbox.setVisible(True)
178
189
  else:
179
190
  self._log_checkbox.setVisible(False)
@@ -188,11 +199,15 @@ class PlotWidget(QWidget):
188
199
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
189
200
  observations: pd.DataFrame,
190
201
  std_dev_images: dict[str, npt.NDArray[np.float32]],
202
+ key_def: PlotApiKeyDefinition | None = None,
191
203
  ) -> None:
192
204
  self.resetPlot()
193
205
  try:
206
+ self._sync_log_checkbox()
194
207
  plot_context.log_scale = (
195
- self._log_checkbox.isVisible() and self._log_checkbox.isChecked()
208
+ self._log_checkbox.isVisible()
209
+ and self._log_checkbox.isChecked()
210
+ and self._negative_values_in_data is False
196
211
  )
197
212
  self._plotter.plot(
198
213
  self._figure,
@@ -200,9 +215,9 @@ class PlotWidget(QWidget):
200
215
  ensemble_to_data_map,
201
216
  observations,
202
217
  std_dev_images,
218
+ key_def,
203
219
  )
204
220
  self._canvas.draw()
205
- self._sync_log_checkbox()
206
221
  except Exception as e:
207
222
  exc_type, _, exc_tb = sys.exc_info()
208
223
  sys.stderr.write("-" * 80 + "\n")
@@ -22,6 +22,7 @@ from PyQt6.QtWidgets import (
22
22
  QWidget,
23
23
  )
24
24
 
25
+ from ert.config.field import Field
25
26
  from ert.dark_storage.common import get_storage_api_version
26
27
  from ert.gui.ertwidgets import CopyButton, showWaitCursorWhileWaiting
27
28
  from ert.services._base_service import ServerBootFail
@@ -39,6 +40,7 @@ from .plottery.plots import (
39
40
  EnsemblePlot,
40
41
  GaussianKDEPlot,
41
42
  HistogramPlot,
43
+ MisfitsPlot,
42
44
  StatisticsPlot,
43
45
  StdDevPlot,
44
46
  )
@@ -50,10 +52,11 @@ ENSEMBLE = "Ensemble"
50
52
  HISTOGRAM = "Histogram"
51
53
  STATISTICS = "Statistics"
52
54
  STD_DEV = "Std Dev"
55
+ MISFITS = "Misfits"
53
56
 
54
57
  RESPONSE_DEFAULT = 0
55
- GEN_KW_DEFAULT = 2
56
- STD_DEV_DEFAULT = 6
58
+ GEN_KW_DEFAULT = 3
59
+ STD_DEV_DEFAULT = 7
57
60
 
58
61
 
59
62
  logger = logging.getLogger(__name__)
@@ -188,6 +191,7 @@ class PlotWindow(QMainWindow):
188
191
 
189
192
  self.addPlotWidget(ENSEMBLE, EnsemblePlot())
190
193
  self.addPlotWidget(STATISTICS, StatisticsPlot())
194
+ self.addPlotWidget(MISFITS, MisfitsPlot())
191
195
  self.addPlotWidget(HISTOGRAM, HistogramPlot())
192
196
  self.addPlotWidget(GAUSSIAN_KDE, GaussianKDEPlot())
193
197
  self.addPlotWidget(DISTRIBUTION, DistributionPlot())
@@ -270,16 +274,25 @@ class PlotWindow(QMainWindow):
270
274
  filter_on=key_def.filter_on,
271
275
  )
272
276
  elif (
273
- key_def.parameter_metadata is not None
274
- and "GEN_KW" in key_def.metadata["data_origin"]
277
+ key_def.parameter is not None
278
+ and key_def.parameter.type == "gen_kw"
275
279
  ):
276
280
  ensemble_to_data_map[ensemble] = self._api.data_for_parameter(
277
281
  ensemble_id=ensemble.id,
278
- parameter_key=key_def.parameter_metadata.key,
282
+ parameter_key=key_def.parameter.name,
279
283
  )
280
284
  except BaseException as e:
281
285
  handle_exception(e)
282
286
 
287
+ negative_values_in_data = False
288
+ if key_def.parameter is not None and key_def.parameter.type == "gen_kw":
289
+ for data in ensemble_to_data_map.values():
290
+ data = data.T
291
+ if data.le(0).any().any():
292
+ negative_values_in_data = True
293
+ break
294
+
295
+ plot_widget._negative_values_in_data = negative_values_in_data
283
296
  observations = None
284
297
  if key_def.observations and selected_ensembles:
285
298
  try:
@@ -290,10 +303,10 @@ class PlotWindow(QMainWindow):
290
303
  handle_exception(e)
291
304
 
292
305
  std_dev_images: dict[str, npt.NDArray[np.float32]] = {}
293
- if "FIELD" in key_def.metadata["data_origin"]:
294
- plot_widget.showLayerWidget.emit(True)
295
306
 
296
- layers = key_def.metadata["ertbox_params"]["nz"]
307
+ if isinstance(key_def.parameter, Field):
308
+ plot_widget.showLayerWidget.emit(True)
309
+ layers = key_def.parameter.ertbox_params.nz
297
310
  plot_widget.updateLayerWidget.emit(layers)
298
311
 
299
312
  if layer is None:
@@ -357,7 +370,11 @@ class PlotWindow(QMainWindow):
357
370
  self._updateCustomizer(plot_widget, self._preferred_ensemble_x_axis_format)
358
371
 
359
372
  plot_widget.updatePlot(
360
- plot_context, ensemble_to_data_map, observations, std_dev_images
373
+ plot_context,
374
+ ensemble_to_data_map,
375
+ observations,
376
+ std_dev_images,
377
+ key_def,
361
378
  )
362
379
 
363
380
  def _updateCustomizer(
@@ -386,15 +403,14 @@ class PlotWindow(QMainWindow):
386
403
  def addPlotWidget(
387
404
  self,
388
405
  name: str,
389
- plotter: (
390
- EnsemblePlot
391
- | StatisticsPlot
392
- | HistogramPlot
393
- | GaussianKDEPlot
394
- | DistributionPlot
395
- | CrossEnsembleStatisticsPlot
396
- | StdDevPlot
397
- ),
406
+ plotter: EnsemblePlot
407
+ | StatisticsPlot
408
+ | HistogramPlot
409
+ | GaussianKDEPlot
410
+ | DistributionPlot
411
+ | CrossEnsembleStatisticsPlot
412
+ | StdDevPlot
413
+ | MisfitsPlot,
398
414
  enabled: bool = True,
399
415
  ) -> None:
400
416
  plot_widget = PlotWidget(name, plotter)
@@ -433,6 +449,7 @@ class PlotWindow(QMainWindow):
433
449
  widget
434
450
  for widget in self._plot_widgets
435
451
  if widget._plotter.dimensionality == key_def.dimensionality
452
+ and (key_def.observations or not widget._plotter.requires_observations)
436
453
  ]
437
454
 
438
455
  # Enabling/disabling tab triggers the
@@ -3,6 +3,7 @@ from .distribution import DistributionPlot
3
3
  from .ensemble import EnsemblePlot
4
4
  from .gaussian_kde import GaussianKDEPlot
5
5
  from .histogram import HistogramPlot
6
+ from .misfits import MisfitsPlot
6
7
  from .statistics import StatisticsPlot
7
8
  from .std_dev import StdDevPlot
8
9
 
@@ -12,6 +13,7 @@ __all__ = [
12
13
  "EnsemblePlot",
13
14
  "GaussianKDEPlot",
14
15
  "HistogramPlot",
16
+ "MisfitsPlot",
15
17
  "StatisticsPlot",
16
18
  "StdDevPlot",
17
19
  ]
@@ -8,7 +8,7 @@ from matplotlib.lines import Line2D
8
8
  from matplotlib.patches import Rectangle
9
9
  from typing_extensions import TypedDict
10
10
 
11
- from ert.gui.tools.plot.plot_api import EnsembleObject
11
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
12
12
 
13
13
  from .plot_tools import ConditionalAxisFormatter, PlotTools
14
14
 
@@ -37,6 +37,7 @@ class CcsData(TypedDict):
37
37
  class CrossEnsembleStatisticsPlot:
38
38
  def __init__(self) -> None:
39
39
  self.dimensionality = 1
40
+ self.requires_observations = False
40
41
 
41
42
  @staticmethod
42
43
  def plot(
@@ -45,6 +46,7 @@ class CrossEnsembleStatisticsPlot:
45
46
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
46
47
  observation_data: pd.DataFrame,
47
48
  std_dev_images: dict[str, npt.NDArray[np.float32]],
49
+ key_def: PlotApiKeyDefinition | None = None,
48
50
  ) -> None:
49
51
  plotCrossEnsembleStatistics(
50
52
  figure, plot_context, ensemble_to_data_map, observation_data
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
 
8
- from ert.gui.tools.plot.plot_api import EnsembleObject
8
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
9
9
 
10
10
  from .plot_tools import ConditionalAxisFormatter, PlotTools
11
11
 
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  class DistributionPlot:
21
21
  def __init__(self) -> None:
22
22
  self.dimensionality = 1
23
+ self.requires_observations = False
23
24
 
24
25
  @staticmethod
25
26
  def plot(
@@ -28,6 +29,7 @@ class DistributionPlot:
28
29
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
29
30
  observation_data: pd.DataFrame,
30
31
  std_dev_images: dict[str, npt.NDArray[np.float32]],
32
+ key_def: PlotApiKeyDefinition | None = None,
31
33
  ) -> None:
32
34
  plotDistribution(figure, plot_context, ensemble_to_data_map, observation_data)
33
35
 
@@ -80,6 +82,9 @@ def plotDistribution(
80
82
  )
81
83
  config.setLegendEnabled(False)
82
84
 
85
+ if plot_context.log_scale:
86
+ axes.set_yscale("log")
87
+
83
88
  PlotTools.finalizePlot(
84
89
  plot_context, figure, axes, default_x_label="Ensemble", default_y_label="Value"
85
90
  )
@@ -15,13 +15,14 @@ if TYPE_CHECKING:
15
15
  from matplotlib.axes import Axes
16
16
  from matplotlib.figure import Figure
17
17
 
18
- from ert.gui.tools.plot.plot_api import EnsembleObject
18
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
19
19
  from ert.gui.tools.plot.plottery import PlotConfig, PlotContext
20
20
 
21
21
 
22
22
  class EnsemblePlot:
23
23
  def __init__(self) -> None:
24
24
  self.dimensionality = 2
25
+ self.requires_observations = False
25
26
 
26
27
  def plot(
27
28
  self,
@@ -30,6 +31,7 @@ class EnsemblePlot:
30
31
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
31
32
  observation_data: pd.DataFrame,
32
33
  std_dev_images: dict[str, npt.NDArray[np.float32]],
34
+ key_def: PlotApiKeyDefinition | None = None,
33
35
  ) -> None:
34
36
  config = plot_context.plotConfig()
35
37
  axes = figure.add_subplot(111)
@@ -6,7 +6,7 @@ import numpy as np
6
6
  import pandas as pd
7
7
  from scipy.stats import gaussian_kde
8
8
 
9
- from ert.gui.tools.plot.plot_api import EnsembleObject
9
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
10
10
 
11
11
  from .plot_tools import ConditionalAxisFormatter, PlotTools
12
12
 
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
21
21
  class GaussianKDEPlot:
22
22
  def __init__(self) -> None:
23
23
  self.dimensionality = 1
24
+ self.requires_observations = False
24
25
 
25
26
  @staticmethod
26
27
  def plot(
@@ -29,10 +30,16 @@ class GaussianKDEPlot:
29
30
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
30
31
  observation_data: pd.DataFrame,
31
32
  std_dev_images: dict[str, npt.NDArray[np.float32]],
33
+ key_def: PlotApiKeyDefinition | None = None,
32
34
  ) -> None:
33
35
  plotGaussianKDE(figure, plot_context, ensemble_to_data_map, observation_data)
34
36
 
35
37
 
38
+ def _array_is_constant(data: pd.Series | pd.DataFrame) -> bool:
39
+ array = data.to_numpy()
40
+ return array.shape[0] == 0 or (array[0] == array).all()
41
+
42
+
36
43
  def plotGaussianKDE(
37
44
  figure: Figure,
38
45
  plot_context: PlotContext,
@@ -55,11 +62,14 @@ def plotGaussianKDE(
55
62
  if data.empty:
56
63
  continue
57
64
  data = data[0]
58
- if data.nunique() > 1:
65
+ if not _array_is_constant(data):
59
66
  _plotGaussianKDE(
60
67
  axes, config, data, f"{ensemble.experiment_name} : {ensemble.name}"
61
68
  )
62
69
 
70
+ if plot_context.log_scale:
71
+ axes.set_xscale("log")
72
+
63
73
  PlotTools.finalizePlot(
64
74
  plot_context, figure, axes, default_x_label="Value", default_y_label="Density"
65
75
  )