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
@@ -6,6 +6,8 @@ import threading
6
6
  import traceback
7
7
  from collections import defaultdict
8
8
  from collections.abc import Awaitable, Callable, Iterable, Sequence
9
+ from dataclasses import dataclass
10
+ from math import ceil
9
11
  from typing import Any, cast, get_args
10
12
 
11
13
  import zmq.asyncio
@@ -15,6 +17,7 @@ from _ert.events import (
15
17
  EESnapshot,
16
18
  EESnapshotUpdate,
17
19
  EnsembleCancelled,
20
+ EnsembleEvaluationWarning,
18
21
  EnsembleFailed,
19
22
  EnsembleStarted,
20
23
  EnsembleSucceeded,
@@ -49,6 +52,13 @@ from .state import (
49
52
  ENSEMBLE_STATE_STOPPED,
50
53
  )
51
54
 
55
+
56
+ @dataclass(order=True)
57
+ class ParallelismViolation:
58
+ amount: float = 0
59
+ message: str = ""
60
+
61
+
52
62
  logger = logging.getLogger(__name__)
53
63
 
54
64
  EVENT_HANDLER = Callable[[list[SnapshotInputEvent]], Awaitable[None]]
@@ -68,6 +78,13 @@ class EventSentinel:
68
78
 
69
79
  class EnsembleEvaluator:
70
80
  BATCHING_INTERVAL = 0.5
81
+ DEFAULT_SLEEP_PERIOD = 0.1
82
+
83
+ # These properties help us determine whether the user
84
+ # has misconfigured NUM_CPU in their config.
85
+ ALLOWED_CPU_OVERSPENDING = 1.05
86
+ MINIMUM_WALLTIME_SECONDS = 30 # Information is only polled every 5 sec
87
+ CPU_OVERSPENDING_WARNING_THRESHOLD = 1.50
71
88
 
72
89
  def __init__(
73
90
  self,
@@ -123,6 +140,7 @@ class EnsembleEvaluator:
123
140
  submit_sleep=self.ensemble._queue_config.submit_sleep,
124
141
  ens_id=self.ensemble.id_,
125
142
  )
143
+ self.max_parallelism_violation = ParallelismViolation()
126
144
 
127
145
  async def _publisher(self) -> None:
128
146
  heartbeat_interval = 0.1
@@ -145,6 +163,11 @@ class EnsembleEvaluator:
145
163
  self._evaluation_result.set_result(True)
146
164
  return
147
165
 
166
+ elif isinstance(event, EnsembleEvaluationWarning):
167
+ if self._event_handler:
168
+ self._event_handler(event)
169
+ self._events_to_send.task_done()
170
+
148
171
  elif type(event) in {
149
172
  EESnapshot,
150
173
  EESnapshotUpdate,
@@ -191,7 +214,7 @@ class EnsembleEvaluator:
191
214
  await self._signal_cancel()
192
215
  logger.debug("Run model cancelled - during evaluation - cancel sent")
193
216
  self._end_event.clear()
194
- await asyncio.sleep(0.1)
217
+ await asyncio.sleep(self.DEFAULT_SLEEP_PERIOD)
195
218
 
196
219
  async def _send_terminate_message_to_dispatchers(self) -> None:
197
220
  event = TERMINATE_MSG
@@ -244,6 +267,7 @@ class EnsembleEvaluator:
244
267
  event_handler[event_type] = func
245
268
 
246
269
  set_event_handler(set(get_args(FMEvent | RealizationEvent)), self._fm_handler)
270
+ set_event_handler({EnsembleEvaluationWarning}, self._warning_event_handler)
247
271
  set_event_handler({EnsembleStarted}, self._started_handler)
248
272
  set_event_handler({EnsembleSucceeded}, self._stopped_handler)
249
273
  set_event_handler({EnsembleCancelled}, self._cancelled_handler)
@@ -264,7 +288,7 @@ class EnsembleEvaluator:
264
288
  batch.append((function, event))
265
289
  self._events.task_done()
266
290
  except asyncio.QueueEmpty:
267
- await asyncio.sleep(0.1)
291
+ await asyncio.sleep(self.DEFAULT_SLEEP_PERIOD)
268
292
  continue
269
293
  self._complete_batch.set()
270
294
  await self._batch_processing_queue.put(batch)
@@ -274,6 +298,12 @@ class EnsembleEvaluator:
274
298
  async def _fm_handler(self, events: Sequence[FMEvent | RealizationEvent]) -> None:
275
299
  await self._append_message(self.ensemble.update_snapshot(events))
276
300
 
301
+ async def _warning_event_handler(
302
+ self, events: Sequence[EnsembleEvaluationWarning]
303
+ ) -> None:
304
+ for event in events:
305
+ await self._events_to_send.put(event)
306
+
277
307
  async def _started_handler(self, events: Sequence[EnsembleStarted]) -> None:
278
308
  if self.ensemble.status != ENSEMBLE_STATE_FAILED:
279
309
  await self._append_message(self.ensemble.update_snapshot(events))
@@ -288,11 +318,9 @@ class EnsembleEvaluator:
288
318
  memory_usage = fm_step.get(ids.MAX_MEMORY_USAGE) or "-1"
289
319
  max_memory_usage = max(int(memory_usage), max_memory_usage)
290
320
 
291
- cpu_message = detect_overspent_cpu(
321
+ self.detect_overspent_cpu(
292
322
  self.ensemble.reals[int(real_id)].num_cpu, real_id, fm_step
293
323
  )
294
- if self.ensemble.queue_system != QueueSystem.LOCAL and cpu_message:
295
- logger.warning(cpu_message)
296
324
 
297
325
  logger.info(
298
326
  "Ensemble ran with maximum memory usage for a "
@@ -368,6 +396,7 @@ class EnsembleEvaluator:
368
396
  logger.warning(
369
397
  "Evaluator receiver closed, no new messages are received"
370
398
  )
399
+ return # The socket is closed, and we won't re-establish it.
371
400
  else:
372
401
  logger.error(f"Unexpected error when listening to messages: {e}")
373
402
  except asyncio.CancelledError:
@@ -416,7 +445,7 @@ class EnsembleEvaluator:
416
445
  while True:
417
446
  if self._evaluation_result.done():
418
447
  break
419
- await asyncio.sleep(0.1)
448
+ await asyncio.sleep(self.DEFAULT_SLEEP_PERIOD)
420
449
  logger.debug("Async server exiting.")
421
450
  finally:
422
451
  try:
@@ -647,27 +676,50 @@ class EnsembleEvaluator:
647
676
  else:
648
677
  await self._events.put(EnsembleCancelled(ensemble=self.ensemble.id_))
649
678
 
650
-
651
- def detect_overspent_cpu(num_cpu: int, real_id: str, fm_step: FMStepSnapshot) -> str:
652
- """Produces a message warning about misconfiguration of NUM_CPU if
653
- so is detected. Returns an empty string if everything is ok."""
654
- allowed_overspending = 1.05
655
- minimum_wallclock_time_seconds = 30 # Information is only polled every 5 sec
656
-
657
- start_time = fm_step.get(ids.START_TIME)
658
- end_time = fm_step.get(ids.END_TIME)
659
- if start_time is None or end_time is None:
660
- return ""
661
- duration = (end_time - start_time).total_seconds()
662
- if duration <= minimum_wallclock_time_seconds:
663
- return ""
664
- cpu_seconds = fm_step.get(ids.CPU_SECONDS) or 0.0
665
- parallelization_obtained = cpu_seconds / duration
666
- if parallelization_obtained > num_cpu * allowed_overspending:
667
- return (
668
- f"Misconfigured NUM_CPU, forward model step '{fm_step.get(ids.NAME)}' for "
669
- f"realization {real_id} spent {cpu_seconds} cpu seconds "
670
- f"with wall clock duration {duration:.1f} seconds, "
671
- f"a factor of {parallelization_obtained:.2f}, while NUM_CPU was {num_cpu}."
679
+ def detect_overspent_cpu(
680
+ self, num_cpu: int, real_id: str, fm_step: FMStepSnapshot
681
+ ) -> None:
682
+ """Produces a message warning about misconfiguration of NUM_CPU if
683
+ so is detected. Returns an empty string if everything is ok."""
684
+ allowed_overspending = self.ALLOWED_CPU_OVERSPENDING * num_cpu
685
+ overspending_warning_threshold = (
686
+ self.CPU_OVERSPENDING_WARNING_THRESHOLD * num_cpu
672
687
  )
673
- return ""
688
+
689
+ start_time = fm_step.get(ids.START_TIME)
690
+
691
+ end_time = fm_step.get(ids.END_TIME)
692
+ if start_time is None or end_time is None:
693
+ return
694
+
695
+ duration = (end_time - start_time).total_seconds()
696
+ if duration <= self.MINIMUM_WALLTIME_SECONDS:
697
+ return
698
+
699
+ cpu_seconds = fm_step.get(ids.CPU_SECONDS) or 0.0
700
+ parallelization_obtained = cpu_seconds / duration
701
+ if (
702
+ parallelization_obtained > allowed_overspending
703
+ and self.ensemble.queue_system != QueueSystem.LOCAL
704
+ ):
705
+ logger.warning(
706
+ f"Misconfigured NUM_CPU, forward model step '{fm_step.get(ids.NAME)}' "
707
+ f"for realization {real_id} spent {cpu_seconds} cpu seconds "
708
+ f"with wall clock duration {duration:.1f} seconds, a factor of "
709
+ f"{parallelization_obtained:.2f}, while NUM_CPU was {num_cpu}."
710
+ )
711
+ if parallelization_obtained > overspending_warning_threshold:
712
+ warning_msg = (
713
+ "Overusage of CPUs detected!\n"
714
+ f"Your experiment has used up to {ceil(parallelization_obtained)} "
715
+ f"CPUs in step '{fm_step.get(ids.NAME)}', "
716
+ f"while the Ert config has only requested {num_cpu}.\n"
717
+ f"This means your experiment is consuming more CPU-resources than "
718
+ f"requested and will slow down other users experiments.\n"
719
+ f"We kindly ask you to set "
720
+ f"NUM_CPU={ceil(parallelization_obtained)} in your Ert config."
721
+ )
722
+ self.max_parallelism_violation = max(
723
+ self.max_parallelism_violation,
724
+ ParallelismViolation(parallelization_obtained, warning_msg),
725
+ )
@@ -1,4 +1,5 @@
1
1
  from collections.abc import Mapping
2
+ from datetime import datetime
2
3
  from typing import Any, Literal
3
4
 
4
5
  from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
@@ -42,6 +43,11 @@ class SnapshotUpdateEvent(_UpdateEvent):
42
43
  event_type: Literal["SnapshotUpdateEvent"] = "SnapshotUpdateEvent"
43
44
 
44
45
 
46
+ class StartEvent(BaseModel):
47
+ event_type: Literal["StartEvent"] = "StartEvent"
48
+ timestamp: datetime
49
+
50
+
45
51
  class EndEvent(BaseModel):
46
52
  model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
47
53
  event_type: Literal["EndEvent"] = "EndEvent"
@@ -12,6 +12,7 @@ from _ert.events import (
12
12
  EESnapshot,
13
13
  EESnapshotUpdate,
14
14
  EnsembleCancelled,
15
+ EnsembleEvaluationWarning,
15
16
  EnsembleEvent,
16
17
  EnsembleFailed,
17
18
  EnsembleStarted,
@@ -424,7 +425,8 @@ class EnsembleSnapshot:
424
425
 
425
426
  elif e_type in get_args(EnsembleEvent):
426
427
  event = cast(EnsembleEvent, event)
427
- self._ensemble_state = _ENSEMBLE_TYPE_EVENT_TO_STATUS[type(event)]
428
+ if not isinstance(event, EnsembleEvaluationWarning):
429
+ self._ensemble_state = _ENSEMBLE_TYPE_EVENT_TO_STATUS[type(event)]
428
430
  elif type(event) is EESnapshotUpdate:
429
431
  self.merge_snapshot(EnsembleSnapshot.from_nested_dict(event.snapshot))
430
432
  elif type(event) is EESnapshot:
@@ -7,6 +7,7 @@ COLOR_RUNNING: Final = (255, 255, 153)
7
7
  COLOR_UNKNOWN: Final = (128, 128, 128)
8
8
  COLOR_WAITING: Final = (164, 200, 255)
9
9
  COLOR_CANCELLED: Final = (235, 242, 246)
10
+ COLOR_WARNING: Final = (255, 103, 0)
10
11
 
11
12
  ENSEMBLE_STATE_CANCELLED: Final = "Cancelled"
12
13
  ENSEMBLE_STATE_FAILED: Final = "Failed"
@@ -4,20 +4,28 @@ from .field_file_format import FieldFileFormat
4
4
  from .field_utils import (
5
5
  ErtboxParameters,
6
6
  Shape,
7
+ calc_rho_for_2d_grid_layer,
7
8
  calculate_ertbox_parameters,
8
9
  get_shape,
10
+ localization_scaling_function,
9
11
  read_field,
10
12
  read_mask,
11
13
  save_field,
14
+ transform_local_ellipse_angle_to_local_coords,
15
+ transform_positions_to_local_field_coordinates,
12
16
  )
13
17
 
14
18
  __all__ = [
15
19
  "ErtboxParameters",
16
20
  "FieldFileFormat",
17
21
  "Shape",
22
+ "calc_rho_for_2d_grid_layer",
18
23
  "calculate_ertbox_parameters",
19
24
  "get_shape",
25
+ "localization_scaling_function",
20
26
  "read_field",
21
27
  "read_mask",
22
28
  "save_field",
29
+ "transform_local_ellipse_angle_to_local_coords",
30
+ "transform_positions_to_local_field_coordinates",
23
31
  ]
@@ -15,7 +15,7 @@ from .roff_io import export_roff, import_roff
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  import numpy.typing as npt
18
- import xtgeo # type: ignore
18
+ import xtgeo
19
19
 
20
20
  _PathLike: TypeAlias = str | os.PathLike[str]
21
21
 
@@ -216,7 +216,6 @@ def calculate_ertbox_parameters(
216
216
  def read_field(
217
217
  field_path: _PathLike,
218
218
  field_name: str,
219
- mask: npt.NDArray[np.bool_],
220
219
  shape: Shape,
221
220
  ) -> np.ma.MaskedArray[Any, np.dtype[np.float32]]:
222
221
  path = Path(field_path)
@@ -239,7 +238,7 @@ def read_field(
239
238
  ext = path.suffix
240
239
  raise ValueError(f'Could not read {field_path}. Unrecognized suffix "{ext}"')
241
240
 
242
- return np.ma.MaskedArray(data=values, mask=mask, fill_value=np.nan) # type: ignore
241
+ return np.ma.MaskedArray(data=values, fill_value=np.nan)
243
242
 
244
243
 
245
244
  def save_field(
@@ -263,3 +262,213 @@ def save_field(
263
262
  export_grdecl(field, output_path, field_name, binary=True)
264
263
  else:
265
264
  raise ValueError(f"Cannot export, invalid file format: {file_format}")
265
+
266
+
267
+ def transform_positions_to_local_field_coordinates(
268
+ coordsys_origin: tuple[float, float],
269
+ coordsys_rotation_angle: float,
270
+ utmx: npt.NDArray[np.float64],
271
+ utmy: npt.NDArray[np.float64],
272
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
273
+ """Calculates coordinate transformation from global to local coordinates.
274
+
275
+ Args:
276
+ coordys_origin: (x,y) coordinate of local coordinate
277
+ origin in global coordinates.
278
+ coordsys_rotation_angle: Angle for how much the local x-axis is rotated
279
+ anti-clockwise relative to the global x-axis in degrees.
280
+ utmx: vector of x-coordinates in global coordinates.
281
+ utmy: vector of y-coordinates in global coordinates.
282
+
283
+ Returns:
284
+ First vector is local x-coordinates and second vector is local y-coordinates.
285
+ """
286
+ # Translate
287
+ x1 = utmx - coordsys_origin[0]
288
+ y1 = utmy - coordsys_origin[1]
289
+ # Rotate
290
+ # Input angle is the local coordinate systems rotation
291
+ # anticlockwise relative to global x-axis in degrees
292
+ rotation_of_ertbox = coordsys_rotation_angle
293
+ rotation_angle = np.deg2rad(rotation_of_ertbox)
294
+ cos_theta = np.cos(rotation_angle)
295
+ sin_theta = np.sin(rotation_angle)
296
+ x2 = x1 * cos_theta + y1 * sin_theta
297
+ y2 = -x1 * sin_theta + y1 * cos_theta
298
+ return x2, y2
299
+
300
+
301
+ def transform_local_ellipse_angle_to_local_coords(
302
+ coordsys_rotation_angle: float,
303
+ ellipse_anisotropy_angle: npt.NDArray[np.float64],
304
+ ) -> npt.NDArray[np.float64]:
305
+ """Calculate angles relative to local coordinate system.
306
+
307
+ Args:
308
+ coordsys_rotation_angle: Local coordinate systems rotation angle
309
+ relative to the global coordinate system.
310
+ ellipse_anisotropy_angle: Vector of input angles in global coordinates.
311
+
312
+ Returns:
313
+ Vector of output angles relative to the local coordinate system.
314
+ """
315
+ # Both angles measured anti-clock from global coordinate systems x-axis in degrees
316
+ return ellipse_anisotropy_angle - coordsys_rotation_angle
317
+
318
+
319
+ def localization_scaling_function(
320
+ distances: npt.NDArray[np.float64],
321
+ ) -> npt.NDArray[np.float64]:
322
+ """Calculate scaling factor to be used as values in
323
+ RHO matrix in distance-based localization.
324
+ The scaling function implements the commonly
325
+ used function published by Gaspari and Cohn.
326
+ For input normalized distance >= 2, the value will be 0.
327
+
328
+ Args:
329
+ distances: Vector of values for normalized distances.
330
+
331
+ Returns:
332
+ Values of scaling factors for each value of input distance.
333
+ """
334
+ # "gaspari-cohn"
335
+ # Commonly used in distance-based localization
336
+ # Is exact 0 for normalized distance > 2.
337
+ scaling_factor = distances
338
+ d2 = distances**2
339
+ d3 = d2 * distances
340
+ d4 = d3 * distances
341
+ d5 = d4 * distances
342
+ s = -1 / 4 * d5 + 1 / 2 * d4 + 5 / 8 * d3 - 5 / 3 * d2 + 1
343
+ scaling_factor[distances <= 1] = s[distances <= 1]
344
+ s = (
345
+ 1 / 12 * d5
346
+ - 1 / 2 * d4
347
+ + 5 / 8 * d3
348
+ + 5 / 3 * d2
349
+ - 5 * distances
350
+ + 4
351
+ - 2 / 3 * 1 / distances
352
+ )
353
+ scaling_factor[(distances > 1) & (distances <= 2)] = s[
354
+ (distances > 1) & (distances <= 2)
355
+ ]
356
+ scaling_factor[distances > 2] = 0.0
357
+
358
+ return scaling_factor
359
+
360
+
361
+ def calc_rho_for_2d_grid_layer(
362
+ nx: int,
363
+ ny: int,
364
+ xinc: float,
365
+ yinc: float,
366
+ obs_xpos: npt.NDArray[np.float64],
367
+ obs_ypos: npt.NDArray[np.float64],
368
+ obs_main_range: npt.NDArray[np.float64],
369
+ obs_perp_range: npt.NDArray[np.float64],
370
+ obs_anisotropy_angle: npt.NDArray[np.float64],
371
+ right_handed_grid_indexing: bool = True,
372
+ ) -> npt.NDArray[np.float64]:
373
+ """Calculate scaling values (RHO matrix elements) for a set of observations
374
+ with associated localization ellipse. The method will first
375
+ calculate the distances from each observation position to each grid cell
376
+ center point of all grid cells for a 2D grid.
377
+ The localization method will only consider lateral distances, and it is
378
+ therefore sufficient to calculate the distances in 2D.
379
+ All input observation positions are in the local grid coordinate system
380
+ to simplify the calculation of the distances.
381
+
382
+ The position: xpos[n], ypos[n] and
383
+ localization ellipse defined by obs_main_range[n],obs_perp_range[n],
384
+ obs_anisotropy_angle[n]) refers to observation[n].
385
+
386
+ The distance between an observation with index n and a grid cell (i,j) is
387
+ d[m,n] = dist((xpos_obs[n],ypos_obs[n]),(xpos_field[i,j],ypos_field[i,j]))
388
+
389
+ RHO[[m,n] = scaling(d)
390
+ where m = j + i * ny for left-handed grid index origo and
391
+ m = (ny - j - 1) + i * ny for right-handed grid index origo
392
+ Note that since d[m,n] does only depend on observation index n and
393
+ grid cell index (i,j). The values for RHO is
394
+ calculated for the combination ((i,j), n) and this covers
395
+ one grid layer in ertbox grid or a 2D surface grid.
396
+
397
+ Args:
398
+ nx: Number of grid cells in x-direction of local coordinate system.
399
+ ny: Number of grid cells in y-direction of local coordinate system.
400
+ xinc: Grid cell size in x-direction.
401
+ yinc: Grid cell size in y-direction.
402
+ obs_xpos: Observations x coordinates in local coordinates
403
+ obs_ypos: Observatiopns y coordinates in local coordinates
404
+ obs_main_range: Localization ellipse first range
405
+ obs_perp_range: Localization ellipse second range
406
+ obs_anisotropy_angle: Localization ellipse orientation relative
407
+ to local coordinate system in degrees
408
+
409
+ Returns:
410
+ Rho matrix values for one layer of the 3D ertbox grid or for a 2D surface grid.
411
+ """
412
+ # Center points of each grid cell in field parameter grid
413
+ x_local = (np.arange(nx, dtype=np.float64) + 0.5) * xinc
414
+ if right_handed_grid_indexing:
415
+ # y coordinate descreases from max to min
416
+ y_local = (np.arange(ny - 1, -1, -1, dtype=np.float64) + 0.5) * yinc
417
+ else:
418
+ # y coordinate increases from min to max
419
+ y_local = (np.arange(ny, dtype=np.float64) + 0.5) * yinc
420
+ mesh_x_coord, mesh_y_coord = np.meshgrid(x_local, y_local, indexing="ij")
421
+
422
+ # Number of observations
423
+ nobs = len(obs_xpos)
424
+ assert nobs == len(obs_ypos), (
425
+ "Number of coordinates must match number of observations"
426
+ )
427
+ assert nobs == len(obs_anisotropy_angle), (
428
+ "Number of ellipse orientation angles must match number of observations"
429
+ )
430
+ assert nobs == len(obs_main_range), (
431
+ "Number of ellipse main range values must match number of observations"
432
+ )
433
+ assert nobs == len(obs_perp_range), (
434
+ "Number of ellipse second range values must match number of observations"
435
+ )
436
+ assert np.all(obs_main_range > 0.0), (
437
+ "All range values for all observations must be positive"
438
+ )
439
+ assert np.all(obs_perp_range > 0.0), (
440
+ "All range values for all observations must be positive"
441
+ )
442
+
443
+ # Expand grid coordinates to match observations
444
+ mesh_x_coord_flat = mesh_x_coord.flatten()[:, np.newaxis] # (nx * ny, 1)
445
+ mesh_y_coord_flat = mesh_y_coord.flatten()[:, np.newaxis] # (nx * ny, 1)
446
+
447
+ # Observation coordinates and parameters
448
+ obs_xpos = obs_xpos[np.newaxis, :] # (1, nobs)
449
+ obs_ypos = obs_ypos[np.newaxis, :] # (1, nobs)
450
+ obs_main_range = obs_main_range[np.newaxis, :] # (1, nobs)
451
+ obs_perp_range = obs_perp_range[np.newaxis, :] # (1, nobs)
452
+ obs_anisotropy_angle = obs_anisotropy_angle[np.newaxis, :] # (1, nobs)
453
+
454
+ # Compute displacement between grid points and observations
455
+ dX = mesh_x_coord_flat - obs_xpos # (nx * ny, nobs)
456
+ dY = mesh_y_coord_flat - obs_ypos # (nx * ny, nobs)
457
+
458
+ # Compute rotation parameters
459
+ rotation = np.deg2rad(obs_anisotropy_angle)
460
+ cos_angle = np.cos(rotation) # (1, nobs)
461
+ sin_angle = np.sin(rotation) # (1, nobs)
462
+
463
+ # Rotate and scale displacements to local coordinate system defined
464
+ # by the two half axes of the influence ellipse. First coordinate (local x) is in
465
+ # direction defined by anisotropy angle and local y is perpendicular to that.
466
+ # Scale the distance by the ranges to get a normalized distance
467
+ # (with value 1 at the edge of the ellipse)
468
+ dX_ellipse = (dX * cos_angle + dY * sin_angle) / obs_main_range # (nx * ny, nobs)
469
+ dY_ellipse = (-dX * sin_angle + dY * cos_angle) / obs_perp_range # (nx * ny, nobs)
470
+
471
+ # Compute distances in the elliptical coordinate system
472
+ distances = np.hypot(dX_ellipse, dY_ellipse) # (nx * ny, nobs)
473
+ # Apply the scaling function
474
+ return localization_scaling_function(distances).reshape((nx, ny, nobs))
@@ -23,7 +23,7 @@ def export_roff(
23
23
  binary: bool,
24
24
  ) -> None:
25
25
  dimensions = data.shape
26
- data = np.flip(data, -1).ravel() # type: ignore
26
+ data = np.flip(data, -1).ravel()
27
27
  data = data.astype(np.float32).filled(RMS_UNDEFINED_FLOAT) # type: ignore
28
28
  if not np.isfinite(data).all():
29
29
  raise ValueError(
ert/gui/__init__.py CHANGED
@@ -20,12 +20,15 @@ else:
20
20
 
21
21
  def is_high_contrast_mode() -> bool:
22
22
  app = cast(QWidget, QApplication.instance())
23
- return app.palette().color(QPalette.ColorRole.Window).lightness() > 245
23
+ return (
24
+ app is not None
25
+ and app.palette().color(QPalette.ColorRole.Window).lightness() > 245
26
+ )
24
27
 
25
28
 
26
29
  def is_dark_mode() -> bool:
27
30
  app = cast(QWidget, QApplication.instance())
28
- return app.palette().base().color().value() < 70
31
+ return app is not None and app.palette().base().color().value() < 70
29
32
 
30
33
 
31
34
  __version__ = ert.shared.__version__
ert/gui/ertnotifier.py CHANGED
@@ -74,7 +74,7 @@ class ErtNotifier(QObject):
74
74
  @Slot()
75
75
  def emitErtChange(self) -> None:
76
76
  if self._storage is not None:
77
- self._storage.refresh()
77
+ self._storage.reload()
78
78
 
79
79
  self.ertChanged.emit()
80
80
 
@@ -1,31 +1,34 @@
1
- # isort: skip_file
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
2
4
  from PyQt6.QtCore import Qt
3
5
  from PyQt6.QtGui import QCursor
4
6
  from PyQt6.QtWidgets import QApplication
5
- from typing import Any
6
- from collections.abc import Callable
7
-
8
7
 
9
- from .closabledialog import ClosableDialog
10
8
  from .analysismoduleedit import AnalysisModuleEdit
11
- from .searchbox import SearchBox
12
- from .ensembleselector import EnsembleSelector
13
9
  from .checklist import CheckList
14
- from .stringbox import StringBox
15
- from .textbox import TextBox
16
- from .listeditbox import ListEditBox
10
+ from .closabledialog import ClosableDialog
11
+ from .copy_button import CopyButton
12
+ from .copyablelabel import CopyableLabel
13
+ from .create_experiment_dialog import CreateExperimentDialog
17
14
  from .customdialog import CustomDialog
18
- from .pathchooser import PathChooser
15
+ from .ensembleselector import EnsembleSelector
16
+ from .listeditbox import ListEditBox
19
17
  from .models import (
20
- TextModel,
21
18
  ActiveRealizationsModel,
19
+ ErtSummary,
20
+ PathModel,
21
+ SelectableListModel,
22
22
  TargetEnsembleModel,
23
+ TextModel,
23
24
  ValueModel,
24
- SelectableListModel,
25
- PathModel,
26
25
  )
27
- from .copyablelabel import CopyableLabel
28
- from .copy_button import CopyButton
26
+ from .parameterviewer import get_parameters_button
27
+ from .pathchooser import PathChooser
28
+ from .searchbox import SearchBox
29
+ from .stringbox import StringBox
30
+ from .suggestor import Suggestor
31
+ from .textbox import TextBox
29
32
 
30
33
 
31
34
  def showWaitCursorWhileWaiting(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -49,17 +52,21 @@ __all__ = [
49
52
  "ClosableDialog",
50
53
  "CopyButton",
51
54
  "CopyableLabel",
55
+ "CreateExperimentDialog",
52
56
  "CustomDialog",
53
57
  "EnsembleSelector",
58
+ "ErtSummary",
54
59
  "ListEditBox",
55
60
  "PathChooser",
56
61
  "PathModel",
57
62
  "SearchBox",
58
63
  "SelectableListModel",
59
64
  "StringBox",
65
+ "Suggestor",
60
66
  "TargetEnsembleModel",
61
67
  "TextBox",
62
68
  "TextModel",
63
69
  "ValueModel",
70
+ "get_parameters_button",
64
71
  "showWaitCursorWhileWaiting",
65
72
  ]
@@ -6,8 +6,8 @@ from PyQt6.QtCore import QMargins, Qt
6
6
  from PyQt6.QtGui import QIcon
7
7
  from PyQt6.QtWidgets import QHBoxLayout, QToolButton, QWidget
8
8
 
9
- from ert.gui.ertwidgets import ClosableDialog
10
- from ert.gui.ertwidgets.analysismodulevariablespanel import AnalysisModuleVariablesPanel
9
+ from .analysismodulevariablespanel import AnalysisModuleVariablesPanel
10
+ from .closabledialog import ClosableDialog
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from ert.config import AnalysisModule
@@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
16
16
  QWidget,
17
17
  )
18
18
 
19
- from ert.gui.ertwidgets import SearchBox
19
+ from .searchbox import SearchBox
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from .models.selectable_list_model import SelectableListModel
@@ -9,9 +9,11 @@ from PyQt6.QtWidgets import (
9
9
  )
10
10
 
11
11
  from ert.gui.ertnotifier import ErtNotifier
12
- from ert.gui.ertwidgets import StringBox, TextModel, ValueModel
13
12
  from ert.validation import ExperimentValidation, IntegerArgument, ProperNameArgument
14
13
 
14
+ from .models import TextModel, ValueModel
15
+ from .stringbox import StringBox
16
+
15
17
 
16
18
  class CreateExperimentDialog(QDialog):
17
19
  onDone = Signal(str, str, int)
@@ -9,11 +9,11 @@ from PyQt6.QtCore import Qt
9
9
  from PyQt6.QtCore import pyqtSignal as Signal
10
10
  from PyQt6.QtWidgets import QComboBox
11
11
 
12
+ from ert.config import ErrorInfo
12
13
  from ert.gui.ertnotifier import ErtNotifier
13
14
  from ert.storage import RealizationStorageState
14
15
 
15
- from ...config import ErrorInfo
16
- from ..suggestor import Suggestor
16
+ from .suggestor import Suggestor
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from ert.storage import Ensemble