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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. _ert/events.py +19 -2
  2. _ert/forward_model_runner/client.py +6 -2
  3. _ert/forward_model_runner/fm_dispatch.py +9 -6
  4. _ert/forward_model_runner/reporting/event.py +1 -0
  5. _ert/forward_model_runner/runner.py +1 -2
  6. _ert/utils.py +12 -0
  7. ert/__main__.py +58 -38
  8. ert/analysis/_enif_update.py +8 -4
  9. ert/analysis/_es_update.py +19 -6
  10. ert/analysis/_update_commons.py +16 -6
  11. ert/base_model_context.py +1 -1
  12. ert/cli/main.py +17 -12
  13. ert/cli/monitor.py +7 -0
  14. ert/config/__init__.py +17 -6
  15. ert/config/_create_observation_dataframes.py +118 -21
  16. ert/config/_get_num_cpu.py +1 -1
  17. ert/config/_observations.py +91 -2
  18. ert/config/_read_summary.py +74 -328
  19. ert/config/design_matrix.py +62 -23
  20. ert/config/distribution.py +1 -1
  21. ert/config/ensemble_config.py +9 -17
  22. ert/config/ert_config.py +155 -58
  23. ert/config/everest_control.py +234 -0
  24. ert/config/{everest_constraints_config.py → everest_response.py} +27 -15
  25. ert/config/field.py +99 -90
  26. ert/config/forward_model_step.py +122 -17
  27. ert/config/gen_data_config.py +5 -10
  28. ert/config/gen_kw_config.py +11 -41
  29. ert/config/known_response_types.py +14 -0
  30. ert/config/parameter_config.py +1 -33
  31. ert/config/parsing/_option_dict.py +10 -2
  32. ert/config/parsing/config_errors.py +1 -1
  33. ert/config/parsing/config_keywords.py +2 -1
  34. ert/config/parsing/config_schema.py +23 -11
  35. ert/config/parsing/config_schema_deprecations.py +3 -3
  36. ert/config/parsing/config_schema_item.py +26 -11
  37. ert/config/parsing/context_values.py +3 -3
  38. ert/config/parsing/file_context_token.py +1 -1
  39. ert/config/parsing/observations_parser.py +6 -2
  40. ert/config/parsing/queue_system.py +9 -0
  41. ert/config/parsing/schema_item_type.py +1 -0
  42. ert/config/queue_config.py +42 -50
  43. ert/config/response_config.py +0 -8
  44. ert/config/rft_config.py +275 -0
  45. ert/config/summary_config.py +3 -8
  46. ert/config/surface_config.py +73 -26
  47. ert/config/workflow_fixtures.py +2 -1
  48. ert/config/workflow_job.py +135 -54
  49. ert/dark_storage/client/__init__.py +2 -2
  50. ert/dark_storage/client/_session.py +4 -4
  51. ert/dark_storage/client/client.py +2 -2
  52. ert/dark_storage/common.py +12 -3
  53. ert/dark_storage/compute/misfits.py +11 -7
  54. ert/dark_storage/endpoints/compute/misfits.py +6 -4
  55. ert/dark_storage/endpoints/ensembles.py +4 -0
  56. ert/dark_storage/endpoints/experiment_server.py +30 -24
  57. ert/dark_storage/endpoints/experiments.py +2 -2
  58. ert/dark_storage/endpoints/observations.py +8 -6
  59. ert/dark_storage/endpoints/parameters.py +4 -12
  60. ert/dark_storage/endpoints/responses.py +24 -5
  61. ert/dark_storage/json_schema/ensemble.py +3 -0
  62. ert/dark_storage/json_schema/experiment.py +1 -1
  63. ert/data/_measured_data.py +6 -5
  64. ert/ensemble_evaluator/__init__.py +8 -1
  65. ert/ensemble_evaluator/config.py +2 -1
  66. ert/ensemble_evaluator/evaluator.py +81 -29
  67. ert/ensemble_evaluator/event.py +6 -0
  68. ert/ensemble_evaluator/snapshot.py +3 -1
  69. ert/ensemble_evaluator/state.py +1 -0
  70. ert/field_utils/__init__.py +8 -0
  71. ert/field_utils/field_utils.py +228 -15
  72. ert/field_utils/grdecl_io.py +1 -1
  73. ert/field_utils/roff_io.py +1 -1
  74. ert/gui/__init__.py +5 -2
  75. ert/gui/ertnotifier.py +1 -1
  76. ert/gui/ertwidgets/__init__.py +23 -16
  77. ert/gui/ertwidgets/analysismoduleedit.py +2 -2
  78. ert/gui/ertwidgets/checklist.py +1 -1
  79. ert/gui/ertwidgets/closabledialog.py +2 -0
  80. ert/gui/ertwidgets/copyablelabel.py +2 -0
  81. ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
  82. ert/gui/ertwidgets/ensembleselector.py +2 -2
  83. ert/gui/ertwidgets/listeditbox.py +2 -0
  84. ert/gui/ertwidgets/models/__init__.py +2 -0
  85. ert/gui/ertwidgets/models/activerealizationsmodel.py +5 -1
  86. ert/gui/ertwidgets/models/path_model.py +1 -1
  87. ert/gui/ertwidgets/models/targetensemblemodel.py +5 -1
  88. ert/gui/ertwidgets/models/text_model.py +4 -1
  89. ert/gui/ertwidgets/pathchooser.py +0 -3
  90. ert/gui/ertwidgets/searchbox.py +17 -4
  91. ert/gui/ertwidgets/stringbox.py +2 -0
  92. ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
  93. ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +63 -30
  94. ert/gui/main.py +41 -13
  95. ert/gui/main_window.py +3 -7
  96. ert/gui/model/fm_step_list.py +3 -0
  97. ert/gui/model/real_list.py +1 -0
  98. ert/gui/model/snapshot.py +1 -0
  99. ert/gui/simulation/combobox_with_description.py +3 -0
  100. ert/gui/simulation/ensemble_experiment_panel.py +8 -2
  101. ert/gui/simulation/ensemble_information_filter_panel.py +7 -2
  102. ert/gui/simulation/ensemble_smoother_panel.py +8 -2
  103. ert/gui/simulation/evaluate_ensemble_panel.py +17 -7
  104. ert/gui/simulation/experiment_panel.py +18 -6
  105. ert/gui/simulation/manual_update_panel.py +35 -10
  106. ert/gui/simulation/multiple_data_assimilation_panel.py +13 -9
  107. ert/gui/simulation/run_dialog.py +47 -20
  108. ert/gui/simulation/single_test_run_panel.py +6 -3
  109. ert/gui/simulation/view/progress_widget.py +2 -0
  110. ert/gui/simulation/view/realization.py +5 -1
  111. ert/gui/simulation/view/update.py +2 -0
  112. ert/gui/summarypanel.py +20 -1
  113. ert/gui/tools/event_viewer/panel.py +3 -4
  114. ert/gui/tools/event_viewer/tool.py +2 -0
  115. ert/gui/tools/load_results/load_results_panel.py +1 -1
  116. ert/gui/tools/load_results/load_results_tool.py +2 -0
  117. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  118. ert/gui/tools/manage_experiments/manage_experiments_panel.py +2 -0
  119. ert/gui/tools/manage_experiments/storage_info_widget.py +121 -16
  120. ert/gui/tools/manage_experiments/storage_widget.py +4 -3
  121. ert/gui/tools/plot/customize/color_chooser.py +5 -2
  122. ert/gui/tools/plot/customize/customize_plot_dialog.py +2 -0
  123. ert/gui/tools/plot/customize/default_customization_view.py +4 -0
  124. ert/gui/tools/plot/customize/limits_customization_view.py +3 -0
  125. ert/gui/tools/plot/customize/statistics_customization_view.py +3 -0
  126. ert/gui/tools/plot/customize/style_chooser.py +2 -0
  127. ert/gui/tools/plot/customize/style_customization_view.py +3 -0
  128. ert/gui/tools/plot/data_type_keys_widget.py +2 -0
  129. ert/gui/tools/plot/data_type_proxy_model.py +3 -0
  130. ert/gui/tools/plot/plot_api.py +50 -28
  131. ert/gui/tools/plot/plot_ensemble_selection_widget.py +17 -10
  132. ert/gui/tools/plot/plot_widget.py +15 -2
  133. ert/gui/tools/plot/plot_window.py +41 -19
  134. ert/gui/tools/plot/plottery/plot_config.py +2 -0
  135. ert/gui/tools/plot/plottery/plot_context.py +14 -0
  136. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  137. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  138. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  139. ert/gui/tools/plot/plottery/plots/ensemble.py +13 -5
  140. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  141. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  142. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  143. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  144. ert/gui/tools/plot/plottery/plots/statistics.py +62 -20
  145. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  146. ert/gui/tools/plot/widgets/clearable_line_edit.py +9 -0
  147. ert/gui/tools/plot/widgets/filter_popup.py +2 -0
  148. ert/gui/tools/plot/widgets/filterable_kw_list_model.py +3 -0
  149. ert/gui/tools/plugins/plugin.py +1 -1
  150. ert/gui/tools/plugins/plugins_tool.py +2 -0
  151. ert/gui/tools/plugins/process_job_dialog.py +3 -0
  152. ert/gui/tools/workflows/workflow_dialog.py +2 -0
  153. ert/gui/tools/workflows/workflows_tool.py +2 -0
  154. ert/libres_facade.py +5 -7
  155. ert/logging/__init__.py +4 -1
  156. ert/mode_definitions.py +2 -0
  157. ert/plugins/__init__.py +4 -6
  158. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  159. ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
  160. ert/plugins/hook_specifications/__init__.py +0 -10
  161. ert/plugins/hook_specifications/jobs.py +0 -9
  162. ert/plugins/plugin_manager.py +53 -124
  163. ert/resources/forward_models/run_reservoirsimulator.py +8 -4
  164. ert/resources/forward_models/template_render.py +10 -10
  165. ert/resources/shell_scripts/delete_directory.py +2 -2
  166. ert/run_models/__init__.py +24 -6
  167. ert/run_models/_create_run_path.py +133 -38
  168. ert/run_models/ensemble_experiment.py +10 -4
  169. ert/run_models/ensemble_information_filter.py +8 -1
  170. ert/run_models/ensemble_smoother.py +9 -3
  171. ert/run_models/evaluate_ensemble.py +8 -6
  172. ert/run_models/event.py +7 -3
  173. ert/run_models/everest_run_model.py +337 -113
  174. ert/run_models/initial_ensemble_run_model.py +25 -24
  175. ert/run_models/manual_update.py +6 -3
  176. ert/run_models/manual_update_enif.py +37 -0
  177. ert/run_models/model_factory.py +78 -18
  178. ert/run_models/multiple_data_assimilation.py +22 -11
  179. ert/run_models/run_model.py +72 -73
  180. ert/run_models/single_test_run.py +7 -4
  181. ert/run_models/update_run_model.py +4 -2
  182. ert/runpaths.py +5 -6
  183. ert/sample_prior.py +9 -4
  184. ert/scheduler/__init__.py +10 -5
  185. ert/scheduler/driver.py +40 -0
  186. ert/scheduler/event.py +3 -1
  187. ert/scheduler/job.py +23 -13
  188. ert/scheduler/lsf_driver.py +15 -5
  189. ert/scheduler/openpbs_driver.py +10 -4
  190. ert/scheduler/scheduler.py +5 -0
  191. ert/scheduler/slurm_driver.py +20 -5
  192. ert/services/__init__.py +2 -2
  193. ert/services/_base_service.py +37 -20
  194. ert/services/_storage_main.py +20 -18
  195. ert/services/ert_server.py +317 -0
  196. ert/shared/_doc_utils/__init__.py +4 -2
  197. ert/shared/_doc_utils/ert_jobs.py +1 -4
  198. ert/shared/net_utils.py +43 -18
  199. ert/shared/storage/connection.py +3 -3
  200. ert/shared/version.py +3 -3
  201. ert/storage/__init__.py +14 -1
  202. ert/storage/local_ensemble.py +44 -13
  203. ert/storage/local_experiment.py +54 -34
  204. ert/storage/local_storage.py +90 -58
  205. ert/storage/migration/to10.py +3 -2
  206. ert/storage/migration/to11.py +9 -10
  207. ert/storage/migration/to12.py +19 -20
  208. ert/storage/migration/to13.py +28 -27
  209. ert/storage/migration/to14.py +3 -3
  210. ert/storage/migration/to15.py +25 -0
  211. ert/storage/migration/to16.py +38 -0
  212. ert/storage/migration/to17.py +42 -0
  213. ert/storage/migration/to18.py +11 -0
  214. ert/storage/migration/to19.py +34 -0
  215. ert/storage/migration/to20.py +23 -0
  216. ert/storage/migration/to21.py +25 -0
  217. ert/storage/migration/to6.py +3 -2
  218. ert/storage/migration/to7.py +12 -13
  219. ert/storage/migration/to8.py +9 -11
  220. ert/storage/migration/to9.py +5 -4
  221. ert/storage/realization_storage_state.py +7 -7
  222. ert/substitutions.py +12 -28
  223. ert/validation/active_range.py +7 -7
  224. ert/validation/ensemble_realizations_argument.py +4 -2
  225. ert/validation/rangestring.py +16 -16
  226. ert/workflow_runner.py +6 -3
  227. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/METADATA +21 -15
  228. ert-19.0.0rc2.dist-info/RECORD +524 -0
  229. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/WHEEL +1 -1
  230. everest/api/everest_data_api.py +14 -1
  231. everest/assets/everest_logo.svg +406 -0
  232. everest/bin/config_branch_script.py +30 -14
  233. everest/bin/everconfigdump_script.py +2 -10
  234. everest/bin/everest_script.py +53 -33
  235. everest/bin/everlint_script.py +3 -5
  236. everest/bin/kill_script.py +7 -5
  237. everest/bin/main.py +11 -24
  238. everest/bin/monitor_script.py +64 -35
  239. everest/bin/utils.py +58 -43
  240. everest/bin/visualization_script.py +23 -13
  241. everest/config/__init__.py +4 -1
  242. everest/config/control_config.py +81 -6
  243. everest/config/control_variable_config.py +4 -3
  244. everest/config/everest_config.py +102 -79
  245. everest/config/forward_model_config.py +5 -3
  246. everest/config/install_data_config.py +7 -5
  247. everest/config/install_job_config.py +45 -3
  248. everest/config/install_template_config.py +3 -3
  249. everest/config/optimization_config.py +19 -6
  250. everest/config/output_constraint_config.py +8 -2
  251. everest/config/server_config.py +6 -55
  252. everest/config/simulator_config.py +62 -17
  253. everest/config/utils.py +25 -105
  254. everest/config/validation_utils.py +34 -15
  255. everest/config_file_loader.py +30 -21
  256. everest/detached/__init__.py +0 -6
  257. everest/detached/client.py +7 -52
  258. everest/detached/everserver.py +19 -45
  259. everest/everest_storage.py +24 -40
  260. everest/gui/everest_client.py +2 -3
  261. everest/gui/main_window.py +2 -2
  262. everest/optimizer/everest2ropt.py +68 -42
  263. everest/optimizer/opt_model_transforms.py +15 -20
  264. everest/optimizer/utils.py +0 -29
  265. everest/plugins/hook_specs.py +0 -24
  266. everest/strings.py +1 -6
  267. everest/util/__init__.py +3 -1
  268. ert/config/everest_objective_config.py +0 -95
  269. ert/config/ext_param_config.py +0 -107
  270. ert/gui/tools/export/__init__.py +0 -3
  271. ert/gui/tools/export/export_panel.py +0 -83
  272. ert/gui/tools/export/export_tool.py +0 -67
  273. ert/gui/tools/export/exporter.py +0 -36
  274. ert/plugins/hook_specifications/ecl_config.py +0 -29
  275. ert/services/storage_service.py +0 -127
  276. ert/summary_key_type.py +0 -234
  277. ert-16.0.9.dist-info/RECORD +0 -521
  278. everest/bin/everexport_script.py +0 -53
  279. everest/config/sampler_config.py +0 -103
  280. everest/simulator/__init__.py +0 -88
  281. everest/simulator/everest_to_ert.py +0 -252
  282. /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
  283. /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
  284. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/entry_points.txt +0 -0
  285. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/licenses/COPYING +0 -0
  286. {ert-16.0.9.dist-info → ert-19.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import json
4
5
  import shutil
5
6
  from collections.abc import Generator
6
7
  from datetime import datetime
8
+ from enum import StrEnum, auto
7
9
  from functools import cached_property
8
10
  from pathlib import Path
9
11
  from typing import TYPE_CHECKING, Annotated, Any
@@ -14,14 +16,11 @@ from pydantic import BaseModel, Field, TypeAdapter
14
16
  from surfio import IrapSurface
15
17
 
16
18
  from ert.config import (
17
- EverestConstraintsConfig,
18
- EverestObjectivesConfig,
19
- ExtParamConfig,
20
- GenDataConfig,
19
+ EverestControl,
21
20
  GenKwConfig,
21
+ KnownResponseTypes,
22
22
  ParameterConfig,
23
23
  ResponseConfig,
24
- SummaryConfig,
25
24
  SurfaceConfig,
26
25
  )
27
26
  from ert.config import Field as FieldConfig
@@ -34,6 +33,20 @@ if TYPE_CHECKING:
34
33
  from .local_storage import LocalStorage
35
34
 
36
35
 
36
+ class ExperimentState(StrEnum):
37
+ pending = auto()
38
+ running = auto()
39
+ completed = auto()
40
+ stopped = auto()
41
+ failed = auto()
42
+ never_run = auto()
43
+
44
+
45
+ class ExperimentStatus(BaseModel):
46
+ message: str = Field(default="")
47
+ status: ExperimentState = Field(default=ExperimentState.pending)
48
+
49
+
37
50
  class _Index(BaseModel):
38
51
  id: UUID
39
52
  name: str
@@ -41,21 +54,19 @@ class _Index(BaseModel):
41
54
  # from a different experiment. For example, a manual update
42
55
  # is a separate experiment from the one that created the prior.
43
56
  ensembles: list[UUID]
57
+ status: ExperimentStatus | None = Field(default=None)
44
58
 
45
59
 
46
60
  _responses_adapter = TypeAdapter( # type: ignore
47
61
  Annotated[
48
- GenDataConfig
49
- | SummaryConfig
50
- | EverestConstraintsConfig
51
- | EverestObjectivesConfig,
62
+ KnownResponseTypes,
52
63
  Field(discriminator="type"),
53
64
  ]
54
65
  )
55
66
 
56
67
  _parameters_adapter = TypeAdapter( # type: ignore
57
68
  Annotated[
58
- (GenKwConfig | SurfaceConfig | FieldConfig | ExtParamConfig),
69
+ (GenKwConfig | SurfaceConfig | FieldConfig | EverestControl),
59
70
  Field(discriminator="type"),
60
71
  ]
61
72
  )
@@ -73,6 +84,7 @@ class LocalExperiment(BaseMode):
73
84
  _responses_file = Path("responses.json")
74
85
  _metadata_file = Path("metadata.json")
75
86
  _templates_file = Path("templates.json")
87
+ _index_file = Path("index.json")
76
88
 
77
89
  def __init__(
78
90
  self,
@@ -97,7 +109,7 @@ class LocalExperiment(BaseMode):
97
109
  self._storage = storage
98
110
  self._path = path
99
111
  self._index = _Index.model_validate_json(
100
- (path / "index.json").read_text(encoding="utf-8")
112
+ (path / self._index_file).read_text(encoding="utf-8")
101
113
  )
102
114
 
103
115
  @classmethod
@@ -147,9 +159,9 @@ class LocalExperiment(BaseMode):
147
159
  name = datetime.today().isoformat()
148
160
 
149
161
  storage._write_transaction(
150
- path / "index.json",
162
+ path / cls._index_file,
151
163
  _Index(id=uuid, name=name, ensembles=[])
152
- .model_dump_json(indent=2)
164
+ .model_dump_json(indent=2, exclude_none=True)
153
165
  .encode("utf-8"),
154
166
  )
155
167
 
@@ -181,9 +193,7 @@ class LocalExperiment(BaseMode):
181
193
 
182
194
  response_data = {}
183
195
  for response in responses or []:
184
- response_data.update(
185
- {response.response_type: response.model_dump(mode="json")}
186
- )
196
+ response_data.update({response.type: response.model_dump(mode="json")})
187
197
  storage._write_transaction(
188
198
  path / cls._responses_file,
189
199
  json.dumps(response_data, default=str, indent=2).encode("utf-8"),
@@ -280,8 +290,7 @@ class LocalExperiment(BaseMode):
280
290
  path = self.mount_point / self._metadata_file
281
291
  if not path.exists():
282
292
  raise ValueError(f"{self._metadata_file!s} does not exist")
283
- with open(path, encoding="utf-8") as f:
284
- return json.load(f)
293
+ return json.loads(path.read_text(encoding="utf-8"))
285
294
 
286
295
  @property
287
296
  def relative_weights(self) -> str:
@@ -295,25 +304,37 @@ class LocalExperiment(BaseMode):
295
304
  def id(self) -> UUID:
296
305
  return self._index.id
297
306
 
307
+ @property
308
+ def status(self) -> ExperimentStatus | None:
309
+ return self._index.status
310
+
311
+ @status.setter
312
+ @require_write
313
+ def status(self, status: ExperimentStatus) -> None:
314
+ if status != self._index.status:
315
+ self._index.status = status
316
+ self._storage._write_transaction(
317
+ self.mount_point / self._index_file,
318
+ self._index.model_dump_json(indent=2).encode("utf-8"),
319
+ )
320
+
298
321
  @property
299
322
  def mount_point(self) -> Path:
300
323
  return self._path
301
324
 
302
325
  @property
303
326
  def parameter_info(self) -> dict[str, Any]:
304
- info: dict[str, Any]
305
- with open(self.mount_point / self._parameter_file, encoding="utf-8") as f:
306
- info = json.load(f)
307
- return info
327
+ return json.loads(
328
+ (self.mount_point / self._parameter_file).read_text(encoding="utf-8")
329
+ )
308
330
 
309
331
  @property
310
332
  def templates_configuration(self) -> list[tuple[str, str]]:
311
333
  templates: list[tuple[str, str]] = []
312
- try:
313
- with open(self.mount_point / self._templates_file, encoding="utf-8") as f:
314
- templates = json.load(f)
315
- except (FileNotFoundError, json.JSONDecodeError):
316
- pass
334
+ with contextlib.suppress(FileNotFoundError, json.JSONDecodeError):
335
+ templates = json.loads(
336
+ (self.mount_point / self._templates_file).read_text(encoding="utf-8")
337
+ )
317
338
  templates_with_content: list[tuple[str, str]] = []
318
339
  for source_file, target_file in templates:
319
340
  try:
@@ -327,10 +348,9 @@ class LocalExperiment(BaseMode):
327
348
 
328
349
  @property
329
350
  def response_info(self) -> dict[str, Any]:
330
- info: dict[str, Any]
331
- with open(self.mount_point / self._responses_file, encoding="utf-8") as f:
332
- info = json.load(f)
333
- return info
351
+ return json.loads(
352
+ (self.mount_point / self._responses_file).read_text(encoding="utf-8")
353
+ )
334
354
 
335
355
  def get_surface(self, name: str) -> IrapSurface:
336
356
  """
@@ -390,7 +410,7 @@ class LocalExperiment(BaseMode):
390
410
 
391
411
  for data in self.response_info.values():
392
412
  response_instance = _responses_adapter.validate_python(data)
393
- responses[response_instance.response_type] = response_instance
413
+ responses[response_instance.type] = response_instance
394
414
 
395
415
  return responses
396
416
 
@@ -423,7 +443,7 @@ class LocalExperiment(BaseMode):
423
443
  mapping = {}
424
444
  for config in self.response_configuration.values():
425
445
  for key in config.keys if config.has_finalized_keys else []:
426
- mapping[key] = config.response_type
446
+ mapping[key] = config.type
427
447
 
428
448
  return mapping
429
449
 
@@ -475,7 +495,7 @@ class LocalExperiment(BaseMode):
475
495
  self._path / self._responses_file,
476
496
  json.dumps(
477
497
  {
478
- c.response_type: c.model_dump(mode="json")
498
+ c.type: c.model_dump(mode="json")
479
499
  for c in responses_configuration.values()
480
500
  },
481
501
  default=str,
@@ -1,19 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import contextlib
4
3
  import json
5
4
  import logging
6
5
  import os
7
6
  import re
8
7
  import shutil
8
+ import types
9
9
  from collections.abc import Generator, MutableSequence
10
10
  from datetime import datetime
11
11
  from functools import cached_property
12
12
  from pathlib import Path
13
13
  from tempfile import NamedTemporaryFile
14
14
  from textwrap import dedent
15
- from types import TracebackType
16
- from typing import Any
15
+ from typing import Any, Self
17
16
  from uuid import UUID, uuid4
18
17
 
19
18
  import polars as pl
@@ -21,6 +20,7 @@ import xarray as xr
21
20
  from filelock import FileLock, Timeout
22
21
  from pydantic import BaseModel, Field
23
22
 
23
+ import ert.storage
24
24
  from ert.config import ErtConfig, ParameterConfig, ResponseConfig
25
25
  from ert.shared import __version__
26
26
 
@@ -31,7 +31,7 @@ from .realization_storage_state import RealizationStorageState
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
- _LOCAL_STORAGE_VERSION = 14
34
+ _LOCAL_STORAGE_VERSION = 21
35
35
 
36
36
 
37
37
  class _Migrations(BaseModel):
@@ -65,8 +65,7 @@ class LocalStorage(BaseMode):
65
65
  self,
66
66
  path: str | os.PathLike[str],
67
67
  mode: Mode,
68
- *,
69
- ignore_migration_check: bool = False,
68
+ stage_for_migration: bool = False,
70
69
  ) -> None:
71
70
  """
72
71
  Initializes the LocalStorage instance.
@@ -77,19 +76,22 @@ class LocalStorage(BaseMode):
77
76
  The file system path to the storage.
78
77
  mode : Mode
79
78
  The access mode for the storage (read/write).
80
- ignore_migration_check : bool
81
- If True, skips migration checks during initialization.
79
+ stage_for_migration : bool
80
+ Whether to avoid reloading storage to allow migration
82
81
  """
83
82
 
84
- super().__init__(mode)
85
83
  self.path = Path(path).absolute()
84
+ super().__init__(mode)
86
85
 
87
- self._experiments: dict[UUID, LocalExperiment]
88
- self._ensembles: dict[UUID, LocalEnsemble]
89
- self._index: _Index
86
+ if mode.can_write:
87
+ self._acquire_lock()
88
+
89
+ self._experiments: dict[UUID, LocalExperiment] = {}
90
+ self._ensembles: dict[UUID, LocalEnsemble] = {}
91
+ self._index: _Index = _Index()
90
92
 
91
93
  try:
92
- version = _storage_version(self.path)
94
+ self.version = _storage_version(self.path)
93
95
  except FileNotFoundError as err:
94
96
  # No index json, will have a problem if other components of storage exists
95
97
  errors = []
@@ -101,29 +103,45 @@ class LocalStorage(BaseMode):
101
103
  errors.append(f"ensemble path: {self.path / self.ENSEMBLES_PATH}")
102
104
  if errors:
103
105
  raise ValueError(f"No index.json, but found: {errors}") from err
104
- version = _LOCAL_STORAGE_VERSION
106
+ self.version = _LOCAL_STORAGE_VERSION
105
107
 
106
- if version > _LOCAL_STORAGE_VERSION:
107
- raise RuntimeError(
108
- f"Cannot open storage '{self.path}': Storage version {version} "
109
- f"is newer than the current version {_LOCAL_STORAGE_VERSION}, "
110
- "upgrade ert to continue, or run with a different ENSPATH"
111
- )
112
- if self.can_write:
113
- self._acquire_lock()
114
- if version < _LOCAL_STORAGE_VERSION and not ignore_migration_check:
115
- self._migrate(version)
116
- self._index = self._load_index()
117
- self._ensure_fs_version_exists()
118
- self._save_index()
119
- elif version < _LOCAL_STORAGE_VERSION:
108
+ if self.check_migration_needed(Path(self.path)) and not self.can_write:
120
109
  raise RuntimeError(
121
110
  f"Cannot open storage '{self.path}' in read-only mode: "
122
- f"Storage version {version} is too old. Run ert to initiate migration."
111
+ f"Storage version {self.version} is too old. "
112
+ f"Run ert to initiate migration."
113
+ )
114
+
115
+ if not stage_for_migration:
116
+ self.reload()
117
+
118
+ if mode.can_write:
119
+ self._save_index()
120
+
121
+ @staticmethod
122
+ def check_migration_needed(storage_dir: Path) -> bool:
123
+ try:
124
+ version = _storage_version(storage_dir)
125
+ except FileNotFoundError:
126
+ version = _LOCAL_STORAGE_VERSION
127
+
128
+ if version > _LOCAL_STORAGE_VERSION:
129
+ raise ert.storage.ErtStorageException(
130
+ f"Cannot open storage '{storage_dir.absolute()}': Storage version "
131
+ f"{version} is newer than the current version {_LOCAL_STORAGE_VERSION}"
132
+ f", upgrade ert to continue, or run with a different ENSPATH"
123
133
  )
124
- self.refresh()
125
134
 
126
- def refresh(self) -> None:
135
+ return version < _LOCAL_STORAGE_VERSION
136
+
137
+ @staticmethod
138
+ def perform_migration(path: Path) -> None:
139
+ if LocalStorage.check_migration_needed(path):
140
+ with LocalStorage(path, Mode("w"), True) as storage:
141
+ storage._migrate(storage.version)
142
+ storage.reload()
143
+
144
+ def reload(self) -> None:
127
145
  """
128
146
  Reloads the index, experiments, and ensembles from the storage.
129
147
 
@@ -207,6 +225,12 @@ class LocalStorage(BaseMode):
207
225
  return _Index.model_validate_json(
208
226
  (self.path / "index.json").read_text(encoding="utf-8")
209
227
  )
228
+ except PermissionError as e:
229
+ logger.error(
230
+ "Permission error when loading index from path: "
231
+ f"{self.path / 'index.json'}. Error: {e}",
232
+ )
233
+ raise e
210
234
  except FileNotFoundError:
211
235
  return _Index()
212
236
 
@@ -218,6 +242,12 @@ class LocalStorage(BaseMode):
218
242
  try:
219
243
  ensemble = LocalEnsemble(self, ensemble_path, self.mode)
220
244
  ensembles.append(ensemble)
245
+ except PermissionError as e:
246
+ logger.error(
247
+ "Permission error when loading ensemble from path: "
248
+ f"{ensemble_path}. Error: {e}",
249
+ )
250
+ raise e
221
251
  except FileNotFoundError:
222
252
  logger.exception(
223
253
  "Failed to load an ensemble from path: %s", ensemble_path
@@ -247,24 +277,17 @@ class LocalStorage(BaseMode):
247
277
  def _swap_path(self) -> Path:
248
278
  return self.path / self.SWAP_PATH
249
279
 
250
- def __enter__(self) -> LocalStorage:
280
+ def __enter__(self) -> Self:
251
281
  return self
252
282
 
253
283
  def __exit__(
254
284
  self,
255
- exception: Exception,
256
- exception_type: type[Exception],
257
- traceback: TracebackType,
285
+ exception: type[BaseException] | None,
286
+ exception_type: BaseException | None,
287
+ traceback: types.TracebackType | None,
258
288
  ) -> None:
259
289
  self.close()
260
290
 
261
- @require_write
262
- def _ensure_fs_version_exists(self) -> None:
263
- # ERT 4 checks that this file exists and if it exists tells the user
264
- # that their ERT storage is incompatible
265
- with contextlib.suppress(FileExistsError):
266
- (self.path / ".fs_version").symlink_to("index.json")
267
-
268
291
  @require_write
269
292
  def _acquire_lock(self) -> None:
270
293
  self._lock = FileLock(self.path / "storage.lock")
@@ -280,22 +303,16 @@ class LocalStorage(BaseMode):
280
303
  def close(self) -> None:
281
304
  """
282
305
  Closes the storage, releasing any acquired locks and saving the index.
283
-
284
306
  This method should be called to cleanly close the storage, especially
285
307
  when it was opened in write mode. Failing to call this method may leave
286
308
  a lock file behind, which would interfere with subsequent access to
287
309
  the storage.
288
310
  """
289
-
290
311
  self._ensembles.clear()
291
312
  self._experiments.clear()
292
313
 
293
- if not self.can_write:
294
- return
295
-
296
- self._save_index()
297
-
298
314
  if self.can_write:
315
+ self._save_index()
299
316
  self._release_lock()
300
317
 
301
318
  def _release_lock(self) -> None:
@@ -493,6 +510,13 @@ class LocalStorage(BaseMode):
493
510
  to12,
494
511
  to13,
495
512
  to14,
513
+ to15,
514
+ to16,
515
+ to17,
516
+ to18,
517
+ to19,
518
+ to20,
519
+ to21,
496
520
  )
497
521
 
498
522
  try:
@@ -522,7 +546,7 @@ class LocalStorage(BaseMode):
522
546
 
523
547
  self._index = self._load_index()
524
548
 
525
- logger.info("storage backed up for version less than 6")
549
+ logger.info("Storage backed up for version less than 5")
526
550
  print(self._legacy_storage_migration_message(bkup_path, "14.6.*"))
527
551
  return None
528
552
  elif version < _LOCAL_STORAGE_VERSION:
@@ -536,6 +560,13 @@ class LocalStorage(BaseMode):
536
560
  11: to12,
537
561
  12: to13,
538
562
  13: to14,
563
+ 14: to15,
564
+ 15: to16,
565
+ 16: to17,
566
+ 17: to18,
567
+ 18: to19,
568
+ 19: to20,
569
+ 20: to21,
539
570
  }
540
571
  for from_version in range(version, _LOCAL_STORAGE_VERSION):
541
572
  migrations[from_version].migrate(self.path)
@@ -553,7 +584,7 @@ class LocalStorage(BaseMode):
553
584
  Get a unique experiment name
554
585
 
555
586
  If an experiment with the given name exists an _0 is appended
556
- or _n+1 where n is the the largest postfix found for the given experiment name
587
+ or _n+1 where n is the largest postfix found for the given experiment name
557
588
  """
558
589
  if not experiment_name:
559
590
  return self.get_unique_experiment_name("default")
@@ -623,16 +654,17 @@ class LocalStorage(BaseMode):
623
654
  def _storage_version(path: Path) -> int:
624
655
  if not path.exists():
625
656
  return _LOCAL_STORAGE_VERSION
626
- try:
627
- with open(path / "index.json", encoding="utf-8") as f:
628
- return int(json.load(f)["version"])
629
- except KeyError as exc:
630
- raise NotImplementedError("Incompatible ERT Local Storage") from exc
631
- except FileNotFoundError:
657
+ if not (path / "index.json").exists():
632
658
  if _is_block_storage(path):
633
659
  return 0
634
660
  else:
635
- raise
661
+ raise FileNotFoundError(path / "index.json")
662
+ try:
663
+ return int(
664
+ json.loads((path / "index.json").read_text(encoding="utf-8"))["version"]
665
+ )
666
+ except KeyError as exc:
667
+ raise NotImplementedError("Incompatible ERT Local Storage") from exc
636
668
 
637
669
 
638
670
  _migration_ert_config: ErtConfig | None = None
@@ -22,8 +22,9 @@ def migrate(path: Path) -> None:
22
22
  shutil.copyfile(incoming_template, template_file_path)
23
23
  templates_abs.append((str(template_file_path.relative_to(experiment)), dst))
24
24
 
25
- with open(experiment / "parameter.json", encoding="utf-8") as fin:
26
- parameters_json = json.load(fin)
25
+ parameters_json = json.loads(
26
+ (experiment / "parameter.json").read_text(encoding="utf-8")
27
+ )
27
28
 
28
29
  for param in parameters_json.values():
29
30
  if param["_ert_kind"] == "GenKwConfig":
@@ -15,18 +15,17 @@ def migrate(path: Path) -> None:
15
15
  ensembles = path.glob("ensembles/*")
16
16
 
17
17
  experiment_id = None
18
- with open(experiment / "index.json", encoding="utf-8") as f:
19
- exp_index = json.load(f)
20
- experiment_id = exp_index["id"]
18
+ exp_index = json.loads((experiment / "index.json").read_text(encoding="utf-8"))
19
+ experiment_id = exp_index["id"]
21
20
 
22
- with open(experiment / "parameter.json", encoding="utf-8") as fin:
23
- parameters_json = json.load(fin)
21
+ parameters_json = json.loads(
22
+ (experiment / "parameter.json").read_text(encoding="utf-8")
23
+ )
24
24
 
25
25
  for ens in ensembles:
26
- with open(ens / "index.json", encoding="utf-8") as f:
27
- ens_file = json.load(f)
28
- if ens_file["experiment_id"] != experiment_id:
29
- continue
26
+ ens_file = json.loads((ens / "index.json").read_text(encoding="utf-8"))
27
+ if ens_file["experiment_id"] != experiment_id:
28
+ continue
30
29
 
31
30
  real_dirs = [*ens.glob("realization-*")]
32
31
  for param_config in parameters_json.values():
@@ -45,7 +44,7 @@ def migrate(path: Path) -> None:
45
44
  array = ds.isel(realizations=0, drop=True)["values"]
46
45
  realization = int(real_dir.name.split("-")[1])
47
46
 
48
- def parse_value(value: float | int | str) -> float | int | str:
47
+ def parse_value(value: float | str) -> float | int | str:
49
48
  if isinstance(value, float | int):
50
49
  return value
51
50
  try:
@@ -25,22 +25,20 @@ def migrate_everest_param(config: dict[str, Any]) -> dict[str, Any]:
25
25
 
26
26
  def migrate(path: Path) -> None:
27
27
  def _replace_ert_kind(file: Path, kind_to_type: dict[str, str]) -> None:
28
- with open(file, encoding="utf-8") as fin:
29
- old_json = json.load(fin)
30
- new_json = {}
28
+ old_json = json.loads(file.read_text(encoding="utf-8"))
29
+ new_json = {}
31
30
 
32
- for key, config in old_json.items():
33
- ert_kind = config.pop("_ert_kind")
31
+ for key, config in old_json.items():
32
+ ert_kind = config.pop("_ert_kind")
34
33
 
35
- if ert_kind == "ExtParamConfig":
36
- new_json[key] = migrate_everest_param(config) | {
37
- "type": "everest_parameters"
38
- }
39
- else:
40
- new_json[key] = config | {"type": kind_to_type[ert_kind]}
34
+ if ert_kind == "ExtParamConfig":
35
+ new_json[key] = migrate_everest_param(config) | {
36
+ "type": "everest_parameters"
37
+ }
38
+ else:
39
+ new_json[key] = config | {"type": kind_to_type[ert_kind]}
41
40
 
42
- with open(file, "w", encoding="utf-8") as fout:
43
- json.dump(new_json, fout, indent=2)
41
+ file.write_text(json.dumps(new_json, indent=2), encoding="utf-8")
44
42
 
45
43
  for experiment in path.glob("experiments/*"):
46
44
  _replace_ert_kind(
@@ -63,12 +61,13 @@ def migrate(path: Path) -> None:
63
61
  },
64
62
  )
65
63
 
66
- with open(experiment / "responses.json", encoding="utf-8") as fin:
67
- old_json = json.load(fin)
68
- new_json = {}
64
+ old_json = json.loads(
65
+ (experiment / "responses.json").read_text(encoding="utf-8")
66
+ )
67
+ new_json = {}
69
68
 
70
- for key, config in old_json.items():
71
- if config["type"] == "summary" and "refcase" in config:
72
- config.pop("refcase")
69
+ for key, config in old_json.items():
70
+ if config["type"] == "summary" and "refcase" in config:
71
+ config.pop("refcase")
73
72
 
74
- new_json[key] = config
73
+ new_json[key] = config