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
ert/config/field.py CHANGED
@@ -3,32 +3,33 @@ from __future__ import annotations
3
3
  import itertools
4
4
  import logging
5
5
  import os
6
- from collections.abc import Iterator
7
- from functools import cached_property
6
+ from collections.abc import Callable, Iterator
8
7
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Literal, Self, cast, overload
8
+ from typing import TYPE_CHECKING, Any, Final, Literal, Self, cast, overload
10
9
 
11
10
  import networkx as nx
12
11
  import numpy as np
13
12
  import xarray as xr
14
- import xtgeo # type: ignore
13
+ import xtgeo
15
14
  from pydantic import field_serializer
16
15
 
17
16
  from ert.field_utils import (
18
17
  ErtboxParameters,
19
18
  FieldFileFormat,
20
19
  Shape,
20
+ calc_rho_for_2d_grid_layer,
21
21
  calculate_ertbox_parameters,
22
22
  get_shape,
23
23
  read_field,
24
- read_mask,
25
24
  save_field,
25
+ transform_local_ellipse_angle_to_local_coords,
26
+ transform_positions_to_local_field_coordinates,
26
27
  )
27
28
  from ert.substitutions import substitute_runpath_name
28
29
  from ert.utils import log_duration
29
30
 
30
31
  from ._str_to_bool import str_to_bool
31
- from .parameter_config import ParameterConfig, ParameterMetadata
32
+ from .parameter_config import ParameterConfig
32
33
  from .parsing import ConfigValidationError, ConfigWarning
33
34
 
34
35
  if TYPE_CHECKING:
@@ -67,32 +68,9 @@ def create_flattened_cube_graph(px: int, py: int, pz: int) -> nx.Graph[int]:
67
68
  return G
68
69
 
69
70
 
70
- def adjust_graph_for_masking(
71
- G: nx.Graph[int], mask: npt.NDArray[np.bool_]
72
- ) -> nx.Graph[int]:
73
- """
74
- Adjust the graph G according to the masking indices.
75
- Removes nodes specified by the mask and relabels the remaining nodes
76
- to have consecutive labels from 0 to G.number_of_nodes - 1.
77
- Parameters:
78
- - G: The graph to adjust
79
- - mask: Boolean mask flattened array
80
- Returns:
81
- - The adjusted graph
82
- """
83
- # Step 1: Remove nodes specified by mask_indices
84
- mask_indices = np.where(mask)[0]
85
- G.remove_nodes_from(mask_indices)
86
-
87
- # Step 2: Relabel remaining nodes to 0, 1, 2, ..., G.number_of_nodes - 1
88
- new_labels = {old_label: new_label for new_label, old_label in enumerate(G.nodes())}
89
- G = nx.relabel_nodes(G, new_labels, copy=True)
90
-
91
- return G
92
-
93
-
94
71
  class Field(ParameterConfig):
95
72
  type: Literal["field"] = "field"
73
+ dimensionality: Literal[3] = 3
96
74
  ertbox_params: ErtboxParameters
97
75
  file_format: FieldFileFormat
98
76
  output_transformation: str | None
@@ -102,31 +80,15 @@ class Field(ParameterConfig):
102
80
  forward_init_file: str
103
81
  output_file: Path
104
82
  grid_file: str
105
- mask_file: Path | None = None
106
83
 
107
84
  @field_serializer("output_file")
108
85
  def serialize_output_file(self, path: Path) -> str:
109
86
  return str(path)
110
87
 
111
- @field_serializer("mask_file")
112
- def serialize_mask_file(self, path: Path | None) -> str | None:
113
- return str(path) if path is not None else None
114
-
115
88
  @property
116
89
  def parameter_keys(self) -> list[str]:
117
90
  return []
118
91
 
119
- @property
120
- def metadata(self) -> list[ParameterMetadata]:
121
- return [
122
- ParameterMetadata(
123
- key=self.name,
124
- transformation=self.output_transformation,
125
- dimensionality=3,
126
- userdata={"data_origin": "FIELD", "ertbox_params": self.ertbox_params},
127
- )
128
- ]
129
-
130
92
  @classmethod
131
93
  def from_config_list(
132
94
  cls,
@@ -239,11 +201,7 @@ class Field(ParameterConfig):
239
201
  )
240
202
 
241
203
  def __len__(self) -> int:
242
- if self.mask_file is None:
243
- return self.ertbox_params.nx * self.ertbox_params.ny * self.ertbox_params.nz
244
-
245
- # Uses int() to convert to standard python int for mypy
246
- return int(np.size(self.mask) - np.count_nonzero(self.mask))
204
+ return self.ertbox_params.nx * self.ertbox_params.ny * self.ertbox_params.nz
247
205
 
248
206
  @log_duration(_logger, custom_name="load_field")
249
207
  def read_from_runpath(
@@ -258,7 +216,6 @@ class Field(ParameterConfig):
258
216
  read_field(
259
217
  run_path / file_name,
260
218
  self.name,
261
- self.mask,
262
219
  Shape(
263
220
  self.ertbox_params.nx,
264
221
  self.ertbox_params.ny,
@@ -294,16 +251,15 @@ class Field(ParameterConfig):
294
251
  from_data: npt.NDArray[np.float64],
295
252
  iens_active_index: npt.NDArray[np.int_],
296
253
  ) -> Iterator[tuple[int, xr.Dataset]]:
254
+ dim_nx, dim_ny, dim_nz = (
255
+ self.ertbox_params.nx,
256
+ self.ertbox_params.ny,
257
+ self.ertbox_params.nz,
258
+ )
259
+
297
260
  for i, realization in enumerate(iens_active_index):
298
- ma = np.ma.MaskedArray( # type: ignore
299
- data=np.zeros(self.mask.size),
300
- mask=self.mask,
301
- fill_value=np.nan,
302
- dtype=from_data.dtype,
303
- )
304
- ma[~ma.mask] = from_data[:, i]
305
- ma = ma.reshape(self.mask.shape) # type: ignore
306
- ds = xr.Dataset({"values": (["x", "y", "z"], ma.filled())})
261
+ values = from_data[:, i].reshape((dim_nx, dim_ny, dim_nz))
262
+ ds = xr.Dataset({"values": (["x", "y", "z"], values)})
307
263
  yield int(realization), ds
308
264
 
309
265
  def load_parameters(
@@ -314,7 +270,7 @@ class Field(ParameterConfig):
314
270
  ensemble_size = len(ds.realizations)
315
271
  da = xr.DataArray(
316
272
  [
317
- np.ma.MaskedArray(data=d, mask=self.mask).compressed() # type: ignore
273
+ np.ma.MaskedArray(data=d).compressed()
318
274
  for d in ds["values"].values.reshape(ensemble_size, -1)
319
275
  ]
320
276
  )
@@ -328,7 +284,7 @@ class Field(ParameterConfig):
328
284
  def _transform_data(
329
285
  self, data_array: xr.DataArray
330
286
  ) -> np.ma.MaskedArray[Any, np.dtype[np.float32]]:
331
- return np.ma.MaskedArray( # type: ignore
287
+ return np.ma.MaskedArray(
332
288
  _field_truncate(
333
289
  field_transform(
334
290
  data_array,
@@ -337,31 +293,18 @@ class Field(ParameterConfig):
337
293
  self.truncation_min,
338
294
  self.truncation_max,
339
295
  ),
340
- self.mask,
341
296
  fill_value=np.nan,
342
297
  )
343
298
 
344
- def save_experiment_data(self, experiment_path: Path) -> None:
345
- mask_path = experiment_path / "grid_mask.npy"
346
- if not mask_path.exists():
347
- mask, _ = read_mask(self.grid_file)
348
- np.save(mask_path, mask)
349
- self.mask_file = mask_path
350
-
351
- @cached_property
352
- def mask(self) -> Any:
353
- if self.mask_file is None:
354
- raise ValueError(
355
- "In order to get Field.mask, Field.save_experiment_data has"
356
- " to be called first"
357
- )
358
- return np.load(self.mask_file)
359
-
360
- def load_parameter_graph(self) -> nx.Graph: # type: ignore
299
+ def load_parameter_graph(self) -> nx.Graph[int]:
361
300
  parameter_graph = create_flattened_cube_graph(
362
301
  px=self.ertbox_params.nx, py=self.ertbox_params.ny, pz=self.ertbox_params.nz
363
302
  )
364
- return adjust_graph_for_masking(G=parameter_graph, mask=self.mask.flatten())
303
+ new_labels = {
304
+ old_label: new_label
305
+ for new_label, old_label in enumerate(parameter_graph.nodes())
306
+ }
307
+ return nx.relabel_nodes(parameter_graph, new_labels, copy=True)
365
308
 
366
309
  @property
367
310
  def nx(self) -> int:
@@ -375,8 +318,77 @@ class Field(ParameterConfig):
375
318
  def nz(self) -> int:
376
319
  return self.ertbox_params.nz
377
320
 
321
+ def calc_rho_for_2d_grid_layer(
322
+ self,
323
+ obs_xpos: npt.NDArray[np.float64],
324
+ obs_ypos: npt.NDArray[np.float64],
325
+ obs_main_range: npt.NDArray[np.float64],
326
+ obs_perp_range: npt.NDArray[np.float64],
327
+ obs_anisotropy_angle: npt.NDArray[np.float64],
328
+ right_handed_grid_indexing: bool = True,
329
+ ) -> npt.NDArray[np.float64]:
330
+ """Function to calculate scaling values to be used in the RHO matrix
331
+ for distance-based localization.
332
+
333
+ Args:
334
+ obs_xpos: x-coordinates in global coordinates of observations
335
+ obs_ypos: y-coordinates in global coordinates of observations
336
+ obs_main_range: Size of influence ellipse main principal direction.
337
+ obs_perp_range: Size of influence ellipse second principal direction.
338
+ obs_anisotropy_angle: Rotation angle anticlock wise of main principal
339
+ direction of influence ellipse relative to global coordinate
340
+ system's x-axis.
341
+ right_handed_grid_indexing: When this is True the field parameters
342
+ grid index order counts J-index down from ny-1 to 0.
343
+ If the value is False, the grid index order is to count J index
344
+ from 0 to ny-1. As standard for 3D field parameters,
345
+ the grid index order follows the right_handed grid indexing.
346
+
347
+ Returns:
348
+ Scaling values (elements of the RHO matrix) as a numpy array
349
+ of shape=(nx,ny,nobservations)
350
+
351
+ """
352
+ # Can only be used if ertbox coordinate system is defined
353
+ assert self.ertbox_params.xinc is not None, (
354
+ "Parameter for grid resolution must be defined"
355
+ )
356
+ assert self.ertbox_params.yinc is not None, (
357
+ "Parameter for grid resolution must be defined"
358
+ )
359
+ assert self.ertbox_params.origin is not None, (
360
+ "Parameter for grid origin must be defined"
361
+ )
362
+ assert self.ertbox_params.rotation_angle is not None, (
363
+ "Parameter for grid rotation must be defined"
364
+ )
365
+ # Transform positions of observations into local coordinates
366
+ xpos, ypos = transform_positions_to_local_field_coordinates(
367
+ self.ertbox_params.origin,
368
+ self.ertbox_params.rotation_angle,
369
+ obs_xpos,
370
+ obs_ypos,
371
+ )
372
+ # Transform localization ellipse orientation to local coordinates
373
+ ellipse_rotation = transform_local_ellipse_angle_to_local_coords(
374
+ self.ertbox_params.rotation_angle, obs_anisotropy_angle
375
+ )
376
+
377
+ return calc_rho_for_2d_grid_layer(
378
+ self.ertbox_params.nx,
379
+ self.ertbox_params.ny,
380
+ self.ertbox_params.xinc,
381
+ self.ertbox_params.yinc,
382
+ xpos,
383
+ ypos,
384
+ obs_main_range,
385
+ obs_perp_range,
386
+ ellipse_rotation,
387
+ right_handed_grid_indexing=right_handed_grid_indexing,
388
+ )
389
+
378
390
 
379
- TRANSFORM_FUNCTIONS = {
391
+ TRANSFORM_FUNCTIONS: Final[dict[str, Callable[[Any], Any]]] = {
380
392
  "LN": np.log,
381
393
  "LOG": np.log,
382
394
  "LN0": lambda v: np.log(v + 0.000001),
@@ -407,7 +419,7 @@ def field_transform(
407
419
  ) -> npt.NDArray[np.float32] | xr.DataArray:
408
420
  if transform_name is None:
409
421
  return data
410
- return TRANSFORM_FUNCTIONS[transform_name](data) # type: ignore
422
+ return TRANSFORM_FUNCTIONS[transform_name](data)
411
423
 
412
424
 
413
425
  def _field_truncate(data: npt.ArrayLike, min_: float | None, max_: float | None) -> Any:
@@ -1,13 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import ClassVar, Literal, NotRequired, Self
5
-
6
- from pydantic import BaseModel, Field, field_validator, model_validator
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Annotated,
7
+ Any,
8
+ ClassVar,
9
+ Literal,
10
+ NotRequired,
11
+ Self,
12
+ cast,
13
+ )
14
+
15
+ from pydantic import (
16
+ BaseModel,
17
+ Field,
18
+ field_validator,
19
+ model_serializer,
20
+ model_validator,
21
+ )
22
+ from pydantic_core.core_schema import ValidationInfo
7
23
  from typing_extensions import TypedDict, Unpack
8
24
 
25
+ from ..base_model_context import BaseModelWithContextSupport
9
26
  from .parsing import ConfigValidationError, ConfigWarning, SchemaItemType
10
27
 
28
+ if TYPE_CHECKING:
29
+ from ert.plugins import ErtRuntimePlugins
30
+
11
31
  logger = logging.getLogger(__name__)
12
32
 
13
33
 
@@ -72,6 +92,7 @@ class ForwardModelStepOptions(TypedDict, total=False):
72
92
  max_running_minutes: NotRequired[int]
73
93
  environment: NotRequired[dict[str, str]]
74
94
  default_mapping: NotRequired[dict[str, str]]
95
+ required_keywords: NotRequired[list[str]]
75
96
 
76
97
 
77
98
  class ForwardModelStepDocumentation(BaseModel):
@@ -80,18 +101,21 @@ class ForwardModelStepDocumentation(BaseModel):
80
101
  source_function_name: str = Field(default="ert")
81
102
  description: str = Field(default="No description")
82
103
  examples: str = Field(default="No examples")
83
- category: (
84
- Literal[
85
- "utility.file_system",
86
- "simulators.reservoir",
87
- "modelling.reservoir",
88
- "utility.templating",
89
- ]
90
- | str
91
- ) = Field(default="Uncategorized")
92
-
93
-
94
- class ForwardModelStep(BaseModel):
104
+ category: Annotated[
105
+ str,
106
+ Field(
107
+ default="Uncategorized",
108
+ examples=[
109
+ "utility.file_system",
110
+ "simulators.reservoir",
111
+ "modelling.reservoir",
112
+ "utility.templating",
113
+ ],
114
+ ),
115
+ ]
116
+
117
+
118
+ class ForwardModelStep(BaseModelWithContextSupport):
95
119
  """
96
120
  Holds information to execute one step of a forward model
97
121
 
@@ -202,7 +226,87 @@ class ForwardModelStep(BaseModel):
202
226
  return None if v == "null" else v
203
227
 
204
228
 
205
- class ForwardModelStepPlugin(ForwardModelStep):
229
+ class UserInstalledForwardModelStep(ForwardModelStep):
230
+ """
231
+ Represents a forward model step installed by a user via the ERT Config
232
+ forward model step format provided via the INSTALL_JOB keyword.
233
+ User-installed forward model steps serialize with their full configuration,
234
+ unlike site-installed steps which only serialize as references.
235
+ """
236
+
237
+ type: Literal["user_installed"] = "user_installed"
238
+
239
+
240
+ class _SerializedSiteInstalledForwardModelStep(TypedDict):
241
+ type: Literal["site_installed"]
242
+ name: str
243
+ private_args: dict[str, str]
244
+
245
+
246
+ class SiteInstalledForwardModelStep(ForwardModelStep):
247
+ """
248
+ Represents a forward model step installed via external plugins.
249
+ Instances of this class serialize only as references to the plugin by name, and
250
+ the user-provided private_args, allowing them to dynamically update when plugins
251
+ change, rather than being locked to a specific executable at serialization time.
252
+ """
253
+
254
+ type: Literal["site_installed"] = "site_installed"
255
+
256
+ @model_serializer(mode="plain")
257
+ def serialize_model(self) -> _SerializedSiteInstalledForwardModelStep:
258
+ return {
259
+ "type": "site_installed",
260
+ "name": self.name,
261
+ "private_args": self.private_args,
262
+ }
263
+
264
+ @model_validator(mode="before")
265
+ @classmethod
266
+ def deserialize_model(
267
+ cls, values: dict[str, Any], info: ValidationInfo
268
+ ) -> dict[str, Any]:
269
+ runtime_plugins = cast("ErtRuntimePlugins", info.context)
270
+ name = values["name"]
271
+
272
+ if runtime_plugins is None:
273
+ if values.get("type") == "site_installed":
274
+ msg = (
275
+ f"Trying to find site-installed forward model step {name} "
276
+ f"without site plugins. This forward model must be loaded "
277
+ f"with ERT site plugins available."
278
+ )
279
+ raise KeyError(msg)
280
+ return values
281
+
282
+ if name not in runtime_plugins.installed_forward_model_steps:
283
+ msg = (
284
+ f"Expected forward model step {name} to be installed "
285
+ f"via plugins, but it was not found. Please check that "
286
+ f"your python environment has it installed."
287
+ )
288
+ raise KeyError(msg)
289
+ site_installed_fm = runtime_plugins.installed_forward_model_steps[name]
290
+
291
+ # Intent: copy the site installed forward model to this instance.
292
+ # bypassing the model_serializer
293
+ site_fm_instance = {
294
+ k: getattr(site_installed_fm, k)
295
+ for k in SiteInstalledForwardModelStep.model_fields
296
+ }
297
+
298
+ return site_fm_instance | (
299
+ {"private_args": values["private_args"]} if "private_args" in values else {}
300
+ )
301
+
302
+
303
+ SiteOrUserForwardModelStep = Annotated[
304
+ (UserInstalledForwardModelStep | SiteInstalledForwardModelStep),
305
+ Field(discriminator="type"),
306
+ ]
307
+
308
+
309
+ class ForwardModelStepPlugin(SiteInstalledForwardModelStep):
206
310
  def __init__(
207
311
  self, name: str, command: list[str], **kwargs: Unpack[ForwardModelStepOptions]
208
312
  ) -> None:
@@ -221,6 +325,7 @@ class ForwardModelStepPlugin(ForwardModelStep):
221
325
  max_running_minutes = kwargs.get("max_running_minutes")
222
326
  environment = kwargs.get("environment", {}) or {}
223
327
  default_mapping = kwargs.get("default_mapping", {}) or {}
328
+ required_keywords = kwargs.get("required_keywords", []) or []
224
329
 
225
330
  super().__init__(
226
331
  name=name,
@@ -235,7 +340,7 @@ class ForwardModelStepPlugin(ForwardModelStep):
235
340
  max_running_minutes=max_running_minutes,
236
341
  min_arg=0,
237
342
  max_arg=0,
238
- required_keywords=[],
343
+ required_keywords=required_keywords,
239
344
  arg_types=[],
240
345
  environment=environment,
241
346
  default_mapping=default_mapping,
@@ -21,7 +21,6 @@ from .responses_index import responses_index
21
21
 
22
22
  class GenDataConfig(ResponseConfig):
23
23
  type: Literal["gen_data"] = "gen_data"
24
- name: str = "gen_data"
25
24
  report_steps_list: list[list[int] | None] = Field(default_factory=list)
26
25
  has_finalized_keys: bool = True
27
26
 
@@ -29,7 +28,7 @@ class GenDataConfig(ResponseConfig):
29
28
  def metadata(self) -> list[ResponseMetadata]:
30
29
  return [
31
30
  ResponseMetadata(
32
- response_type=self.name,
31
+ response_type=self.type,
33
32
  response_key=response_key,
34
33
  finalized=self.has_finalized_keys,
35
34
  filter_on={"report_step": report_steps}
@@ -198,28 +197,24 @@ class GenDataConfig(ResponseConfig):
198
197
  if all(isinstance(err, FileNotFoundError) for err in errors):
199
198
  raise FileNotFoundError(
200
199
  "Could not find one or more files/directories while reading "
201
- f"GEN_DATA {self.name}: {','.join([str(err) for err in errors])}"
200
+ f"GEN_DATA: {','.join([str(err) for err in errors])}"
202
201
  )
203
202
  else:
204
203
  raise InvalidResponseFile(
205
204
  "Error reading GEN_DATA "
206
- f"{self.name}, errors: {','.join([str(err) for err in errors])}"
205
+ f"{self.type}, errors: {','.join([str(err) for err in errors])}"
207
206
  )
208
207
 
209
208
  combined = pl.concat(datasets_per_name)
210
209
  return combined
211
210
 
212
211
  def get_args_for_key(self, key: str) -> tuple[str | None, list[int] | None]:
213
- for i, _key in enumerate(self.keys):
214
- if key == _key:
212
+ for i, key_ in enumerate(self.keys):
213
+ if key == key_:
215
214
  return self.input_files[i], self.report_steps_list[i]
216
215
 
217
216
  return None, None
218
217
 
219
- @property
220
- def response_type(self) -> str:
221
- return "gen_data"
222
-
223
218
  @property
224
219
  def primary_key(self) -> list[str]:
225
220
  return ["report_step", "index"]
@@ -15,7 +15,7 @@ from typing_extensions import TypedDict
15
15
 
16
16
  from ._str_to_bool import str_to_bool
17
17
  from .distribution import DISTRIBUTION_CLASSES, DistributionSettings, get_distribution
18
- from .parameter_config import ParameterCardinality, ParameterConfig, ParameterMetadata
18
+ from .parameter_config import ParameterCardinality, ParameterConfig
19
19
  from .parsing import ConfigValidationError, ConfigWarning
20
20
 
21
21
  if TYPE_CHECKING:
@@ -53,6 +53,7 @@ class DataSource(StrEnum):
53
53
 
54
54
  class GenKwConfig(ParameterConfig):
55
55
  type: Literal["gen_kw"] = "gen_kw"
56
+ dimensionality: Literal[1] = 1
56
57
  distribution: DistributionSettings
57
58
  forward_init: bool = False
58
59
  update: bool = True
@@ -73,17 +74,6 @@ class GenKwConfig(ParameterConfig):
73
74
  def cardinality(self) -> ParameterCardinality:
74
75
  return ParameterCardinality.multiple_configs_per_ensemble_dataset
75
76
 
76
- @property
77
- def metadata(self) -> list[ParameterMetadata]:
78
- return [
79
- ParameterMetadata(
80
- key=f"{self.group}:{self.name}",
81
- transformation=self.distribution.name.upper(),
82
- dimensionality=1,
83
- userdata={"data_origin": "GEN_KW"},
84
- )
85
- ]
86
-
87
77
  @classmethod
88
78
  def templates_from_config(
89
79
  cls, gen_kw: list[str | dict[str, str]]
@@ -121,7 +111,7 @@ class GenKwConfig(ParameterConfig):
121
111
  gen_kw_key = cast(str, config_list[0])
122
112
 
123
113
  options = cast(dict[str, str], config_list[-1])
124
- positional_args = cast(list[str], config_list[:-1])
114
+ positional_args = cast(list[str | list[str]], config_list[:-1])
125
115
  errors = []
126
116
  update_parameter = str_to_bool(options.get("UPDATE", "TRUE"))
127
117
  if _get_abs_path(options.get("INIT_FILES")):
@@ -186,7 +176,7 @@ class GenKwConfig(ParameterConfig):
186
176
  params[0], params[1], params[2:]
187
177
  ),
188
178
  forward_init=False,
189
- update=update_parameter,
179
+ update="CONST" not in params and update_parameter,
190
180
  )
191
181
  for params in distributions_spec
192
182
  ]
@@ -217,18 +207,7 @@ class GenKwConfig(ParameterConfig):
217
207
  real_nr: int,
218
208
  ensemble: Ensemble,
219
209
  ) -> dict[str, dict[str, float | str]]:
220
- df = ensemble.load_parameters(self.name, real_nr, transformed=True).drop(
221
- "realization"
222
- )
223
-
224
- assert isinstance(df, pl.DataFrame)
225
- if not df.width == 1:
226
- raise ValueError(
227
- f"GEN_KW {self.group_name}:{self.name} should be a single parameter!"
228
- )
229
-
230
- data = df.to_dicts()[0]
231
- return {self.group_name: data}
210
+ raise NotImplementedError
232
211
 
233
212
  def load_parameters(
234
213
  self, ensemble: Ensemble, realizations: npt.NDArray[np.int_]
@@ -259,15 +238,6 @@ class GenKwConfig(ParameterConfig):
259
238
  def group_name(self) -> str:
260
239
  return self.group
261
240
 
262
- def copy_parameters(
263
- self,
264
- source_ensemble: Ensemble,
265
- target_ensemble: Ensemble,
266
- realizations: npt.NDArray[np.int_],
267
- ) -> None:
268
- df = source_ensemble.load_parameters(self.name, realizations)
269
- target_ensemble.save_parameters(dataset=df)
270
-
271
241
  def get_priors(self) -> list[PriorDict]:
272
242
  dist_json = self.distribution.model_dump(exclude={"name"})
273
243
  return [
@@ -0,0 +1,14 @@
1
+ from .everest_response import EverestConstraintsConfig, EverestObjectivesConfig
2
+ from .gen_data_config import GenDataConfig
3
+ from .rft_config import RFTConfig
4
+ from .summary_config import SummaryConfig
5
+
6
+ KnownErtResponseTypes = SummaryConfig | GenDataConfig | RFTConfig
7
+ KNOWN_ERT_RESPONSE_TYPES = (
8
+ SummaryConfig,
9
+ GenDataConfig,
10
+ RFTConfig,
11
+ )
12
+ KnownResponseTypes = (
13
+ KnownErtResponseTypes | EverestConstraintsConfig | EverestObjectivesConfig
14
+ )
@@ -5,7 +5,7 @@ from collections.abc import Callable, Iterator
5
5
  from enum import StrEnum, auto
6
6
  from hashlib import sha256
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any, Literal
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  import networkx as nx
11
11
  import numpy as np
@@ -39,13 +39,6 @@ class ParameterCardinality(StrEnum):
39
39
  one_config_per_realization_dataset = auto()
40
40
 
41
41
 
42
- class ParameterMetadata(BaseModel):
43
- key: str
44
- transformation: str | None
45
- dimensionality: Literal[1, 2, 3] = 1
46
- userdata: dict[str, Any]
47
-
48
-
49
42
  class ParameterConfig(BaseModel):
50
43
  type: str
51
44
  name: str
@@ -59,14 +52,6 @@ class ParameterConfig(BaseModel):
59
52
  Returns a list of parameter keys within this parameter group
60
53
  """
61
54
 
62
- @property
63
- @abstractmethod
64
- def metadata(self) -> list[ParameterMetadata]:
65
- """
66
- Returns metadata describing this parameter
67
-
68
- """
69
-
70
55
  @abstractmethod
71
56
  def __len__(self) -> int:
72
57
  """Number of parameters"""
@@ -108,23 +93,6 @@ class ParameterConfig(BaseModel):
108
93
  or polars DataFrame from the numpy data
109
94
  """
110
95
 
111
- def copy_parameters(
112
- self,
113
- source_ensemble: Ensemble,
114
- target_ensemble: Ensemble,
115
- realizations: npt.NDArray[np.int_],
116
- ) -> None:
117
- """
118
- Copy parameters from one ensemble to another.
119
- If realizations is None, copy all realizations.
120
- If realizations is given, copy only those realizations.
121
- """
122
- for realization in realizations:
123
- # Converts to standard python scalar due to mypy
124
- realization_int = int(realization)
125
- ds = source_ensemble.load_parameters(self.name, realization_int)
126
- target_ensemble.save_parameters(ds, self.name, realization_int)
127
-
128
96
  @abstractmethod
129
97
  def load_parameters(
130
98
  self, ensemble: Ensemble, realizations: npt.NDArray[np.int_]