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
@@ -0,0 +1,436 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal, cast
4
+
5
+ import matplotlib.dates as mdates
6
+ import numpy as np
7
+ import pandas as pd
8
+ import polars as pl
9
+ import seaborn as sns
10
+ from matplotlib import pyplot as plt
11
+ from matplotlib.lines import Line2D
12
+
13
+ if TYPE_CHECKING:
14
+ import numpy.typing as npt
15
+ from matplotlib.figure import Figure
16
+
17
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
18
+ from ert.gui.tools.plot.plottery import PlotContext
19
+
20
+
21
+ class MisfitsPlot:
22
+ """
23
+ Visualize signed chi-squared misfits between simulated responses and observations.
24
+
25
+ Layout:
26
+ - X-axis: index for gen data, time for summary
27
+ - Y-axis: signed chi-squared misfits
28
+ - Glyphs: One boxplot per time step / gendata index
29
+ """
30
+
31
+ def __init__(self) -> None:
32
+ self.dimensionality = 2
33
+ self.requires_observations = True
34
+
35
+ @staticmethod
36
+ def _fade_axis_ticklabels(ax: plt.Axes) -> None:
37
+ """Apply the same alpha to all tick labels on an axis."""
38
+ for label in (*ax.get_xticklabels(), *ax.get_yticklabels()):
39
+ label.set_alpha(0.4)
40
+
41
+ ax.xaxis.label.set_alpha(0.4)
42
+ ax.yaxis.label.set_alpha(0.4)
43
+
44
+ @staticmethod
45
+ def _draw_legend(
46
+ figure: Figure,
47
+ ensemble_colors: dict[tuple[str, str], str],
48
+ sorted_ensemble_keys: list[tuple[str, str]],
49
+ ) -> None:
50
+ legend_handles = [
51
+ Line2D(
52
+ [],
53
+ [],
54
+ marker="s",
55
+ linestyle="None",
56
+ color=ensemble_colors[key],
57
+ label=key[0],
58
+ )
59
+ for key in sorted_ensemble_keys
60
+ ]
61
+ figure.legend(
62
+ handles=legend_handles,
63
+ loc="upper center",
64
+ bbox_to_anchor=(0.5, 0.93),
65
+ ncol=min(len(legend_handles), 4),
66
+ )
67
+
68
+ @staticmethod
69
+ def _make_ensemble_colors(
70
+ sorted_ensemble_keys: list[tuple[str, str]],
71
+ ) -> dict[tuple[str, str], str]:
72
+ """
73
+ Build a consistent color map and figure-level legend
74
+ used by all misfit plots
75
+ """
76
+ colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
77
+ return {
78
+ key: colors[i % len(colors)] for i, key in enumerate(sorted_ensemble_keys)
79
+ }
80
+
81
+ @staticmethod
82
+ def _compute_misfits_padded_minmax(
83
+ misfits_df: pl.DataFrame, relative_pad_y_axis: float
84
+ ) -> tuple[float, float]:
85
+ y_min, y_max = (
86
+ cast(float, misfits_df["misfit"].min()),
87
+ cast(float, misfits_df["misfit"].max()),
88
+ )
89
+ abs_pad_y = (y_max - y_min) * relative_pad_y_axis
90
+ y_min -= abs_pad_y
91
+ y_max += abs_pad_y
92
+
93
+ return y_min, y_max
94
+
95
+ @staticmethod
96
+ def _wide_pandas_to_long_polars_with_misfits(
97
+ ensemble_to_data_map: dict[tuple[str, str], pd.DataFrame],
98
+ observation_data: pd.DataFrame,
99
+ response_type: Literal["summary", "gen_data"],
100
+ ) -> dict[tuple[str, str], pl.DataFrame]:
101
+ if response_type == "summary":
102
+ key_index_with_correct_dtype = pl.col("key_index").str.to_datetime(
103
+ strict=False
104
+ )
105
+ elif response_type == "gen_data":
106
+ key_index_with_correct_dtype = (
107
+ pl.col("key_index").cast(pl.Float32).cast(pl.UInt16)
108
+ )
109
+ else:
110
+ raise ValueError(f"Unsupported response_type: {response_type}")
111
+
112
+ obs_df = (
113
+ pl.from_pandas(observation_data.T)
114
+ .rename({"OBS": "observation", "STD": "error"})
115
+ .with_columns(pl.col("key_index").cast(pl.String))
116
+ .with_columns(key_index_with_correct_dtype)
117
+ )
118
+
119
+ return {
120
+ ens_key: (
121
+ pl.from_pandas(df, include_index=True)
122
+ .unpivot(
123
+ index=df.index.name,
124
+ variable_name="key_index",
125
+ value_name="response",
126
+ )
127
+ .with_columns(key_index_with_correct_dtype)
128
+ .join(obs_df, on="key_index", how="inner")
129
+ .with_columns(
130
+ (pl.col("response") - pl.col("observation")).alias("residual")
131
+ )
132
+ .with_columns(
133
+ (
134
+ pl.col("residual").sign()
135
+ * (pl.col("residual") / pl.col("error")).pow(2)
136
+ ).alias("misfit")
137
+ )
138
+ .drop("residual")
139
+ )
140
+ for ens_key, df in ensemble_to_data_map.items()
141
+ }
142
+
143
+ def plot(
144
+ self,
145
+ figure: Figure,
146
+ plot_context: PlotContext,
147
+ ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
148
+ observation_data: pd.DataFrame,
149
+ std_dev_images: dict[str, npt.NDArray[np.float32]],
150
+ key_def: PlotApiKeyDefinition | None = None,
151
+ ) -> None:
152
+ assert key_def is not None
153
+ response_type = key_def.metadata["data_origin"]
154
+ data_with_misfits = self._wide_pandas_to_long_polars_with_misfits(
155
+ {(eo.name, eo.id): df for eo, df in ensemble_to_data_map.items()},
156
+ observation_data,
157
+ response_type,
158
+ )
159
+
160
+ if response_type == "summary":
161
+ self._plot_summary_misfits_boxplots(
162
+ figure,
163
+ data_with_misfits,
164
+ plot_context,
165
+ )
166
+
167
+ elif response_type == "gen_data":
168
+ self._plot_gendata_misfits(
169
+ figure,
170
+ data_with_misfits,
171
+ plot_context,
172
+ )
173
+
174
+ def _plot_gendata_misfits(
175
+ self,
176
+ figure: Figure,
177
+ data_with_misfits: dict[tuple[str, str], pl.DataFrame],
178
+ plot_context: PlotContext,
179
+ ) -> None:
180
+ # Only plot ensembles with data (i.e., they pertain to an experiment
181
+ # with observations, and there are responses towards those in the ens)
182
+ ensemble_to_misfit_df = {
183
+ k: v for k, v in data_with_misfits.items() if not v.is_empty()
184
+ }
185
+ sorted_ensemble_keys = sorted(ensemble_to_misfit_df.keys())
186
+
187
+ all_misfits = pl.concat(
188
+ [
189
+ df.with_columns(pl.lit(key).alias("ensemble_key"))
190
+ for key, df in ensemble_to_misfit_df.items()
191
+ ]
192
+ ).select(["realization", "key_index", "misfit", "ensemble_key"])
193
+
194
+ distinct_gendata_index = all_misfits["key_index"].unique().sort().to_list()
195
+ num_gendata_index = len(distinct_gendata_index)
196
+
197
+ color_map = self._make_ensemble_colors(sorted_ensemble_keys)
198
+ self._draw_legend(
199
+ figure=figure,
200
+ ensemble_colors=color_map,
201
+ sorted_ensemble_keys=sorted_ensemble_keys,
202
+ )
203
+
204
+ y_min, y_max = self._compute_misfits_padded_minmax(all_misfits, 0.05)
205
+
206
+ # Create subplot grid (2 rows, N columns)
207
+ axes = figure.subplots(
208
+ nrows=2, ncols=num_gendata_index, sharex="col", sharey=True
209
+ )
210
+ axes = (
211
+ axes.reshape(2, num_gendata_index)
212
+ if num_gendata_index > 1
213
+ else np.array([[axes[0]], [axes[1]]])
214
+ )
215
+ axes_top, axes_bottom = axes[0, :], axes[1, :]
216
+
217
+ x_positions = np.arange(len(sorted_ensemble_keys))
218
+ box_width_relative = 0.6
219
+
220
+ for col_idx, key_index in enumerate(distinct_gendata_index):
221
+ ax_top, ax_bottom = axes_top[col_idx], axes_bottom[col_idx]
222
+
223
+ for ens_idx, ens_key in enumerate(sorted_ensemble_keys):
224
+ color = color_map.get(ens_key, "C0")
225
+
226
+ # Filter for the specific key and ensemble
227
+ mis_vals = all_misfits.filter(
228
+ (pl.col("key_index") == key_index)
229
+ & (pl.col("ensemble_key") == ens_key)
230
+ )["misfit"].to_numpy()
231
+
232
+ if mis_vals.size == 0:
233
+ continue
234
+
235
+ x_center = x_positions[ens_idx]
236
+
237
+ # Top: Boxplot
238
+ ax_top.boxplot(
239
+ mis_vals,
240
+ positions=[x_center],
241
+ widths=box_width_relative,
242
+ patch_artist=True,
243
+ showfliers=False,
244
+ boxprops={"facecolor": color, "alpha": 0.35},
245
+ whiskerprops={"color": color, "alpha": 0.8},
246
+ capprops={"color": color, "alpha": 0.8},
247
+ medianprops={"color": color, "alpha": 0.8},
248
+ )
249
+ ax_top.plot(x_center, np.mean(mis_vals), "o", markersize=4, color=color)
250
+
251
+ # Bottom: Strip plot with dynamic marker size
252
+ num_points = len(mis_vals)
253
+
254
+ if num_points >= 200:
255
+ marker_size = 2
256
+ elif num_points >= 100:
257
+ marker_size = 3
258
+ else:
259
+ marker_size = 4
260
+
261
+ # Use stripplot as a robust alternative for dense data
262
+ sns.stripplot(
263
+ x=[x_center] * num_points, # Plot all points at the same x-center
264
+ y=mis_vals,
265
+ ax=ax_bottom,
266
+ color=color,
267
+ size=marker_size,
268
+ alpha=0.35,
269
+ jitter=True, # Explicitly add jitter
270
+ )
271
+
272
+ # Axis/spine styling
273
+ (n_rows, n_cols) = axes.shape
274
+ for r_idx in range(n_rows):
275
+ for c_idx in range(n_cols):
276
+ ax = axes[r_idx, c_idx]
277
+ is_first_col = c_idx == 0
278
+ is_bottom_row = r_idx == (n_rows - 1)
279
+
280
+ # Common styling
281
+ ax.set(ylim=(y_min, y_max))
282
+ ax.axhline(0.0, color="black", linewidth=0.5, alpha=0.5)
283
+ ax.grid(True, axis="y", linestyle=":", alpha=0.4)
284
+ self._fade_axis_ticklabels(ax)
285
+
286
+ # Spines
287
+ ax.spines["top"].set_visible(False)
288
+ ax.spines["right"].set_visible(False)
289
+ ax.spines["left"].set_visible(is_first_col)
290
+ ax.spines["bottom"].set_visible(is_bottom_row)
291
+
292
+ # Ticks
293
+ ax.set_xticks(x_positions, labels=[])
294
+ if not is_bottom_row:
295
+ ax.tick_params(axis="x", which="both", bottom=False)
296
+ if not is_first_col:
297
+ ax.tick_params(axis="y", which="both", left=False)
298
+
299
+ for ax, key_val in zip(axes_bottom, distinct_gendata_index, strict=True):
300
+ ax.set_xlabel(f"index={int(key_val)}", rotation=25, ha="right")
301
+
302
+ figure.suptitle(
303
+ f"{plot_context.key()} (Signed Chi-squared misfits per index)",
304
+ fontsize=14,
305
+ y=0.98,
306
+ )
307
+ figure.tight_layout(rect=(0.02, 0.02, 0.98, 0.88))
308
+
309
+ def _plot_summary_misfits_boxplots(
310
+ self,
311
+ figure: Figure,
312
+ data_with_misfits: dict[tuple[str, str], pl.DataFrame],
313
+ plot_context: PlotContext,
314
+ ) -> None:
315
+ # Calculate shared y-axis limits from all misfits
316
+ all_misfits = pl.concat(
317
+ [df.select("misfit") for df in data_with_misfits.values()]
318
+ )
319
+ y_min, y_max = self._compute_misfits_padded_minmax(all_misfits, 0.05)
320
+
321
+ # Prepare ensemble colors and draw the legend
322
+ sorted_ensemble_keys = sorted(data_with_misfits.keys())
323
+ color_map = self._make_ensemble_colors(sorted_ensemble_keys)
324
+ self._draw_legend(
325
+ figure=figure,
326
+ ensemble_colors=color_map,
327
+ sorted_ensemble_keys=sorted_ensemble_keys,
328
+ )
329
+
330
+ # Create all subplots at once with shared axes
331
+ n_ens = len(sorted_ensemble_keys)
332
+ axes = figure.subplots(nrows=n_ens, ncols=1, sharex=True, sharey=True)
333
+ axes = [axes] if n_ens == 1 else axes.tolist()
334
+ axes[0].set_ylim(y_min, y_max)
335
+
336
+ for ax, ensemble_key in zip(axes, sorted_ensemble_keys, strict=True):
337
+ df = data_with_misfits[ensemble_key]
338
+ if df.is_empty():
339
+ continue
340
+
341
+ df = df.select(["key_index", "misfit"]).sort("key_index")
342
+ times_py = df["key_index"].unique(maintain_order=True).to_list()
343
+ positions = mdates.date2num(times_py) # type: ignore[no-untyped-call]
344
+
345
+ # Calculate dynamic box width based on time spacing
346
+ min_dt = np.min(np.diff(positions)) if len(positions) > 1 else 1.0
347
+
348
+ # multiplier to downsize outlier sizes etc
349
+ # (without this, outliers, whiskers etc are sized way
350
+ # out of proportion when there are many tiny boxplots)
351
+ many_boxes_factor = min(1, len(times_py) / 50)
352
+ box_width = min_dt * (0.7 - 0.3 * (1 - many_boxes_factor))
353
+
354
+ # One boxplot per time step
355
+ grouped_misfits = df.group_by("key_index", maintain_order=True).agg(
356
+ pl.col("misfit")
357
+ )
358
+ data_for_boxes = [
359
+ s.to_numpy() if s.len() > 0 else np.array([np.nan])
360
+ for s in grouped_misfits["misfit"]
361
+ ]
362
+
363
+ color = color_map.get(ensemble_key)
364
+
365
+ # Draw the boxplots with inlined styles
366
+ bp = ax.boxplot(
367
+ data_for_boxes,
368
+ positions=positions,
369
+ widths=box_width,
370
+ whis=(5, 95),
371
+ showfliers=True,
372
+ manage_ticks=False,
373
+ patch_artist=True,
374
+ boxprops={
375
+ "facecolor": color,
376
+ "alpha": 0.18,
377
+ "edgecolor": color,
378
+ "linewidth": 0.7,
379
+ },
380
+ whiskerprops={"color": color, "alpha": 0.6, "linewidth": 0.7},
381
+ capprops={"color": color, "alpha": 0.6, "linewidth": 0.7},
382
+ medianprops={"color": color, "linewidth": 0.6, "alpha": 0.9},
383
+ flierprops={
384
+ "marker": "o",
385
+ "markersize": min(6, box_width * (0.4 - (0.2 * many_boxes_factor))),
386
+ "alpha": 0.7,
387
+ "markeredgewidth": 0.3 + (0.4 * (1 - many_boxes_factor)),
388
+ "markeredgecolor": color,
389
+ "markerfacecolor": "none",
390
+ },
391
+ )
392
+ plt.setp(bp["fliers"], zorder=1.5) # Put fliers behind other elements
393
+
394
+ # Add ensemble name text and a horizontal line at y=0
395
+ ax.text(
396
+ 0.01,
397
+ 0.85,
398
+ f"{ensemble_key[0]}",
399
+ transform=ax.transAxes,
400
+ ha="left",
401
+ va="center",
402
+ fontsize=9,
403
+ alpha=0.75,
404
+ )
405
+ ax.axhline(0.0, color="black", linewidth=0.8, alpha=0.4, zorder=0)
406
+
407
+ # Apply common styling to all axes
408
+ for ax in axes:
409
+ ax.spines["top"].set_visible(False)
410
+ ax.spines["right"].set_visible(False)
411
+ ax.spines["left"].set_visible(True)
412
+ ax.tick_params(axis="y", labelsize=10, width=0.5, length=3)
413
+ ax.grid(True, axis="y", linestyle=":", linewidth=0.5, alpha=0.75)
414
+ self._fade_axis_ticklabels(ax)
415
+
416
+ # Hide the x-axis on all but the last plot
417
+ for ax in axes[:-1]:
418
+ ax.spines["bottom"].set_visible(False)
419
+ ax.tick_params(axis="x", which="both", bottom=False, labelbottom=False)
420
+
421
+ # Style the x-axis only on the last plot
422
+ bottom_ax = axes[-1]
423
+ bottom_ax.spines["bottom"].set_visible(True)
424
+ bottom_ax.xaxis.set_major_locator(mdates.AutoDateLocator()) # type: ignore[no-untyped-call]
425
+ bottom_ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d")) # type: ignore[no-untyped-call]
426
+
427
+ bottom_ax.tick_params(axis="x", labelsize=8, width=0.5, length=3)
428
+ plt.setp(bottom_ax.get_xticklabels(), rotation=25, ha="right")
429
+
430
+ figure.suptitle(
431
+ f"{plot_context.key()} (Signed Chi-squared misfits over time)",
432
+ fontsize=14,
433
+ y=0.97,
434
+ alpha=0.9,
435
+ )
436
+ figure.tight_layout(rect=(0.02, 0.02, 0.98, 0.92))
@@ -27,7 +27,10 @@ def plotObservations(
27
27
 
28
28
 
29
29
  def _plotObservations(
30
- axes: Axes, plot_config: PlotConfig, data: DataFrame, value_column: str
30
+ axes: Axes,
31
+ plot_config: PlotConfig,
32
+ data: DataFrame,
33
+ value_column: str,
31
34
  ) -> None:
32
35
  """
33
36
  Observations are always plotted on top. z-order set to 1000
@@ -46,11 +49,22 @@ def _plotObservations(
46
49
  # line style set to 'off' toggles errorbar visibility
47
50
  if not style.line_style:
48
51
  style.width = 0
52
+ if plot_config.depth_y_axis:
53
+ errorbar_data = {
54
+ "x": data.loc["OBS"].to_numpy(),
55
+ "y": data.loc["key_index"].to_numpy(),
56
+ "xerr": data.loc["STD"].to_numpy(),
57
+ }
58
+ axes.yaxis.set_inverted(True)
59
+ else:
60
+ errorbar_data = {
61
+ "x": data.loc["key_index"].to_numpy(),
62
+ "y": data.loc["OBS"].to_numpy(),
63
+ "yerr": data.loc["STD"].to_numpy(),
64
+ }
49
65
 
50
66
  axes.errorbar(
51
- x=data.loc["key_index"].values,
52
- y=data.loc["OBS"].values,
53
- yerr=data.loc["STD"].values,
67
+ **errorbar_data,
54
68
  fmt=style.line_style,
55
69
  ecolor=style.color,
56
70
  color=style.color,
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  import numpy as np
6
6
  from matplotlib.lines import Line2D
@@ -18,12 +18,13 @@ if TYPE_CHECKING:
18
18
  from matplotlib.axes import Axes
19
19
  from matplotlib.figure import Figure
20
20
 
21
- from ert.gui.tools.plot.plot_api import EnsembleObject
21
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
22
22
 
23
23
 
24
24
  class StatisticsPlot:
25
25
  def __init__(self) -> None:
26
26
  self.dimensionality = 2
27
+ self.requires_observations = False
27
28
 
28
29
  @staticmethod
29
30
  def plot(
@@ -32,6 +33,7 @@ class StatisticsPlot:
32
33
  ensemble_to_data_map: dict[EnsembleObject, DataFrame],
33
34
  observation_data: DataFrame,
34
35
  std_dev_images: dict[str, npt.NDArray[np.float32]],
36
+ key_def: PlotApiKeyDefinition | None = None,
35
37
  ) -> None:
36
38
  config = plot_context.plotConfig()
37
39
  axes = figure.add_subplot(111)
@@ -126,10 +128,18 @@ def _plotPercentiles(
126
128
  axes: Axes, plot_config: PlotConfig, data: DataFrame, ensemble_label: str
127
129
  ) -> None:
128
130
  style = plot_config.getStatisticsStyle("mean")
131
+ if plot_config.depth_y_axis:
132
+ axes.yaxis.set_inverted(True)
133
+
134
+ def xy_order(x: Any, y: Any) -> tuple[Any, Any]:
135
+ if plot_config.depth_y_axis:
136
+ return (y, x)
137
+ else:
138
+ return (x, y)
139
+
129
140
  if style.isVisible():
130
141
  axes.plot(
131
- data.index.values,
132
- data["Mean"].values,
142
+ *xy_order(data.index.values, data["Mean"].values),
133
143
  alpha=style.alpha,
134
144
  linestyle=style.line_style,
135
145
  color=style.color,
@@ -141,8 +151,7 @@ def _plotPercentiles(
141
151
  style = plot_config.getStatisticsStyle("p50")
142
152
  if style.isVisible():
143
153
  axes.plot(
144
- data.index.values,
145
- data["p50"].values,
154
+ *xy_order(data.index.values, data["p50"].values),
146
155
  alpha=style.alpha,
147
156
  linestyle=style.line_style,
148
157
  color=style.color,
@@ -153,7 +162,13 @@ def _plotPercentiles(
153
162
 
154
163
  style = plot_config.getStatisticsStyle("std")
155
164
  _plotPercentile(
156
- axes, style, data.index.values, data["std+"].values, data["std-"].values, 0.5
165
+ axes,
166
+ style,
167
+ data.index.values,
168
+ data["std+"].values,
169
+ data["std-"].values,
170
+ 0.5,
171
+ plot_config.depth_y_axis,
157
172
  )
158
173
 
159
174
  style = plot_config.getStatisticsStyle("min-max")
@@ -164,16 +179,29 @@ def _plotPercentiles(
164
179
  data["Maximum"].values,
165
180
  data["Minimum"].values,
166
181
  0.5,
182
+ plot_config.depth_y_axis,
167
183
  )
168
184
 
169
185
  style = plot_config.getStatisticsStyle("p10-p90")
170
186
  _plotPercentile(
171
- axes, style, data.index.values, data["p90"].values, data["p10"].values, 0.5
187
+ axes,
188
+ style,
189
+ data.index.values,
190
+ data["p90"].values,
191
+ data["p10"].values,
192
+ 0.5,
193
+ plot_config.depth_y_axis,
172
194
  )
173
195
 
174
196
  style = plot_config.getStatisticsStyle("p33-p67")
175
197
  _plotPercentile(
176
- axes, style, data.index.values, data["p67"].values, data["p33"].values, 0.5
198
+ axes,
199
+ style,
200
+ data.index.values,
201
+ data["p67"].values,
202
+ data["p33"].values,
203
+ 0.5,
204
+ plot_config.depth_y_axis,
177
205
  )
178
206
 
179
207
 
@@ -184,24 +212,39 @@ def _plotPercentile(
184
212
  top_line_data: npt.ArrayLike,
185
213
  bottom_line_data: npt.ArrayLike,
186
214
  alpha_multiplier: float,
215
+ inverted: bool = False,
187
216
  ) -> None:
188
217
  alpha = style.alpha
189
218
  line_style = style.line_style
190
219
  color = style.color
191
220
  marker = style.marker
192
221
 
222
+ def xy_order(x: Any, y: Any) -> tuple[Any, Any]:
223
+ if inverted:
224
+ return (y, x)
225
+ else:
226
+ return (x, y)
227
+
193
228
  if line_style == "#":
194
- axes.fill_between(
195
- index_values,
196
- bottom_line_data,
197
- top_line_data,
198
- alpha=alpha * alpha_multiplier,
199
- color=color,
200
- )
229
+ if inverted:
230
+ axes.fill_betweenx(
231
+ index_values,
232
+ bottom_line_data,
233
+ top_line_data,
234
+ alpha=alpha * alpha_multiplier,
235
+ color=color,
236
+ )
237
+ else:
238
+ axes.fill_between(
239
+ index_values,
240
+ bottom_line_data,
241
+ top_line_data,
242
+ alpha=alpha * alpha_multiplier,
243
+ color=color,
244
+ )
201
245
  elif style.isVisible():
202
246
  axes.plot(
203
- index_values,
204
- bottom_line_data,
247
+ *xy_order(index_values, bottom_line_data),
205
248
  alpha=alpha,
206
249
  linestyle=line_style,
207
250
  color=color,
@@ -210,8 +253,7 @@ def _plotPercentile(
210
253
  markersize=style.size,
211
254
  )
212
255
  axes.plot(
213
- index_values,
214
- top_line_data,
256
+ *xy_order(index_values, top_line_data),
215
257
  alpha=alpha,
216
258
  linestyle=line_style,
217
259
  color=color,
@@ -10,13 +10,14 @@ from matplotlib.figure import Figure
10
10
  from mpl_toolkits.axes_grid1 import make_axes_locatable
11
11
 
12
12
  if TYPE_CHECKING:
13
- from ert.gui.tools.plot.plot_api import EnsembleObject
13
+ from ert.gui.tools.plot.plot_api import EnsembleObject, PlotApiKeyDefinition
14
14
  from ert.gui.tools.plot.plottery import PlotContext
15
15
 
16
16
 
17
17
  class StdDevPlot:
18
18
  def __init__(self) -> None:
19
19
  self.dimensionality = 3
20
+ self.requires_observations = False
20
21
 
21
22
  def plot(
22
23
  self,
@@ -25,6 +26,7 @@ class StdDevPlot:
25
26
  ensemble_to_data_map: dict[EnsembleObject, pd.DataFrame],
26
27
  observation_data: pd.DataFrame,
27
28
  std_dev_data: dict[str, npt.NDArray[np.float32]],
29
+ key_def: PlotApiKeyDefinition | None = None,
28
30
  ) -> None:
29
31
  ensemble_count = len(plot_context.ensembles())
30
32
  layer = plot_context.layer
ert/mode_definitions.py CHANGED
@@ -6,6 +6,7 @@ TEST_RUN_MODE = "test_run"
6
6
  WORKFLOW_MODE = "workflow"
7
7
  EVALUATE_ENSEMBLE_MODE = "evaluate_ensemble"
8
8
  MANUAL_UPDATE_MODE = "manual_update"
9
+ MANUAL_ENIF_UPDATE_MODE = "manual_enif_update"
9
10
 
10
11
  MODULE_MODE = {
11
12
  "EnsembleInformationFilter": ENIF_MODE,
@@ -15,4 +16,5 @@ MODULE_MODE = {
15
16
  "SingleTestRun": TEST_RUN_MODE,
16
17
  "EvaluateEnsemble": EVALUATE_ENSEMBLE_MODE,
17
18
  "ManualUpdate": MANUAL_UPDATE_MODE,
19
+ "ManualEnIFUpdate": MANUAL_ENIF_UPDATE_MODE,
18
20
  }
ert/plugins/__init__.py CHANGED
@@ -25,7 +25,6 @@ def plugin(name: str) -> Callable[[Callable[P, Any]], Callable[P, Any]]:
25
25
  if (
26
26
  func.__name__
27
27
  in {
28
- "installable_jobs",
29
28
  "job_documentation",
30
29
  "installable_workflow_jobs",
31
30
  "help_links",