ert 17.1.7__py3-none-any.whl → 18.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. _ert/events.py +19 -2
  2. ert/__main__.py +8 -7
  3. ert/analysis/_enif_update.py +8 -4
  4. ert/analysis/_update_commons.py +16 -6
  5. ert/cli/main.py +6 -3
  6. ert/cli/monitor.py +7 -0
  7. ert/config/__init__.py +13 -3
  8. ert/config/_create_observation_dataframes.py +60 -12
  9. ert/config/_observations.py +14 -1
  10. ert/config/_read_summary.py +8 -6
  11. ert/config/ensemble_config.py +6 -14
  12. ert/config/ert_config.py +19 -13
  13. ert/config/{everest_objective_config.py → everest_response.py} +23 -12
  14. ert/config/ext_param_config.py +133 -1
  15. ert/config/field.py +12 -8
  16. ert/config/forward_model_step.py +108 -6
  17. ert/config/gen_data_config.py +2 -6
  18. ert/config/gen_kw_config.py +0 -9
  19. ert/config/known_response_types.py +14 -0
  20. ert/config/parameter_config.py +0 -17
  21. ert/config/parsing/config_keywords.py +1 -0
  22. ert/config/parsing/config_schema.py +12 -0
  23. ert/config/parsing/config_schema_deprecations.py +11 -0
  24. ert/config/parsing/config_schema_item.py +1 -1
  25. ert/config/queue_config.py +4 -4
  26. ert/config/response_config.py +0 -7
  27. ert/config/rft_config.py +230 -0
  28. ert/config/summary_config.py +2 -6
  29. ert/config/violations.py +0 -0
  30. ert/config/workflow_fixtures.py +2 -1
  31. ert/dark_storage/client/__init__.py +2 -2
  32. ert/dark_storage/client/_session.py +4 -4
  33. ert/dark_storage/client/client.py +2 -2
  34. ert/dark_storage/compute/misfits.py +7 -6
  35. ert/dark_storage/endpoints/compute/misfits.py +2 -2
  36. ert/dark_storage/endpoints/observations.py +4 -4
  37. ert/dark_storage/endpoints/responses.py +15 -1
  38. ert/ensemble_evaluator/__init__.py +8 -1
  39. ert/ensemble_evaluator/evaluator.py +81 -29
  40. ert/ensemble_evaluator/event.py +6 -0
  41. ert/ensemble_evaluator/snapshot.py +3 -1
  42. ert/ensemble_evaluator/state.py +1 -0
  43. ert/field_utils/__init__.py +8 -0
  44. ert/field_utils/field_utils.py +211 -1
  45. ert/gui/ertwidgets/__init__.py +23 -16
  46. ert/gui/ertwidgets/analysismoduleedit.py +2 -2
  47. ert/gui/ertwidgets/checklist.py +1 -1
  48. ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
  49. ert/gui/ertwidgets/ensembleselector.py +2 -2
  50. ert/gui/ertwidgets/models/__init__.py +2 -0
  51. ert/gui/ertwidgets/models/activerealizationsmodel.py +2 -1
  52. ert/gui/ertwidgets/models/path_model.py +1 -1
  53. ert/gui/ertwidgets/models/targetensemblemodel.py +2 -1
  54. ert/gui/ertwidgets/models/text_model.py +1 -1
  55. ert/gui/ertwidgets/searchbox.py +13 -4
  56. ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
  57. ert/gui/main.py +11 -6
  58. ert/gui/main_window.py +1 -2
  59. ert/gui/simulation/ensemble_experiment_panel.py +1 -1
  60. ert/gui/simulation/ensemble_information_filter_panel.py +1 -1
  61. ert/gui/simulation/ensemble_smoother_panel.py +1 -1
  62. ert/gui/simulation/evaluate_ensemble_panel.py +1 -1
  63. ert/gui/simulation/experiment_panel.py +1 -1
  64. ert/gui/simulation/manual_update_panel.py +31 -8
  65. ert/gui/simulation/multiple_data_assimilation_panel.py +12 -8
  66. ert/gui/simulation/run_dialog.py +25 -4
  67. ert/gui/simulation/single_test_run_panel.py +2 -2
  68. ert/gui/summarypanel.py +1 -1
  69. ert/gui/tools/load_results/load_results_panel.py +1 -1
  70. ert/gui/tools/manage_experiments/storage_info_widget.py +7 -7
  71. ert/gui/tools/manage_experiments/storage_widget.py +1 -2
  72. ert/gui/tools/plot/plot_api.py +13 -10
  73. ert/gui/tools/plot/plot_window.py +12 -0
  74. ert/gui/tools/plot/plottery/plot_config.py +2 -0
  75. ert/gui/tools/plot/plottery/plot_context.py +14 -0
  76. ert/gui/tools/plot/plottery/plots/ensemble.py +9 -2
  77. ert/gui/tools/plot/plottery/plots/statistics.py +59 -19
  78. ert/mode_definitions.py +2 -0
  79. ert/plugins/__init__.py +0 -1
  80. ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
  81. ert/plugins/hook_specifications/__init__.py +0 -2
  82. ert/plugins/hook_specifications/jobs.py +0 -9
  83. ert/plugins/plugin_manager.py +2 -33
  84. ert/resources/shell_scripts/delete_directory.py +2 -2
  85. ert/run_models/__init__.py +18 -5
  86. ert/run_models/_create_run_path.py +56 -23
  87. ert/run_models/ensemble_experiment.py +10 -4
  88. ert/run_models/ensemble_information_filter.py +8 -1
  89. ert/run_models/ensemble_smoother.py +9 -3
  90. ert/run_models/evaluate_ensemble.py +8 -6
  91. ert/run_models/event.py +7 -3
  92. ert/run_models/everest_run_model.py +155 -44
  93. ert/run_models/initial_ensemble_run_model.py +23 -22
  94. ert/run_models/manual_update.py +4 -2
  95. ert/run_models/manual_update_enif.py +37 -0
  96. ert/run_models/model_factory.py +81 -22
  97. ert/run_models/multiple_data_assimilation.py +21 -10
  98. ert/run_models/run_model.py +54 -34
  99. ert/run_models/single_test_run.py +7 -4
  100. ert/run_models/update_run_model.py +4 -2
  101. ert/runpaths.py +5 -6
  102. ert/sample_prior.py +9 -4
  103. ert/scheduler/driver.py +37 -0
  104. ert/scheduler/event.py +3 -1
  105. ert/scheduler/job.py +23 -13
  106. ert/scheduler/lsf_driver.py +6 -2
  107. ert/scheduler/openpbs_driver.py +7 -1
  108. ert/scheduler/scheduler.py +5 -0
  109. ert/scheduler/slurm_driver.py +6 -2
  110. ert/services/__init__.py +2 -2
  111. ert/services/_base_service.py +31 -15
  112. ert/services/ert_server.py +317 -0
  113. ert/shared/_doc_utils/ert_jobs.py +1 -4
  114. ert/shared/storage/connection.py +3 -3
  115. ert/shared/version.py +3 -3
  116. ert/storage/local_ensemble.py +25 -5
  117. ert/storage/local_experiment.py +6 -14
  118. ert/storage/local_storage.py +35 -30
  119. ert/storage/migration/to18.py +12 -0
  120. ert/storage/migration/to8.py +4 -4
  121. ert/substitutions.py +12 -28
  122. ert/validation/active_range.py +7 -7
  123. ert/validation/rangestring.py +16 -16
  124. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/METADATA +8 -7
  125. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/RECORD +160 -159
  126. everest/bin/config_branch_script.py +3 -6
  127. everest/bin/everconfigdump_script.py +1 -9
  128. everest/bin/everest_script.py +21 -11
  129. everest/bin/kill_script.py +2 -2
  130. everest/bin/monitor_script.py +2 -2
  131. everest/bin/utils.py +6 -3
  132. everest/config/__init__.py +4 -1
  133. everest/config/control_config.py +61 -2
  134. everest/config/control_variable_config.py +2 -1
  135. everest/config/everest_config.py +38 -16
  136. everest/config/forward_model_config.py +5 -3
  137. everest/config/install_data_config.py +7 -5
  138. everest/config/install_job_config.py +7 -3
  139. everest/config/install_template_config.py +3 -3
  140. everest/config/optimization_config.py +19 -6
  141. everest/config/output_constraint_config.py +8 -2
  142. everest/config/server_config.py +6 -49
  143. everest/config/utils.py +25 -105
  144. everest/config/validation_utils.py +10 -10
  145. everest/config_file_loader.py +13 -2
  146. everest/detached/everserver.py +7 -8
  147. everest/everest_storage.py +6 -10
  148. everest/gui/everest_client.py +0 -1
  149. everest/gui/main_window.py +2 -2
  150. everest/optimizer/everest2ropt.py +59 -32
  151. everest/optimizer/opt_model_transforms.py +12 -13
  152. everest/optimizer/utils.py +0 -29
  153. everest/strings.py +0 -5
  154. ert/config/everest_constraints_config.py +0 -95
  155. ert/services/storage_service.py +0 -127
  156. everest/config/sampler_config.py +0 -103
  157. everest/simulator/__init__.py +0 -88
  158. everest/simulator/everest_to_ert.py +0 -51
  159. /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
  160. /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
  161. /ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +0 -0
  162. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/WHEEL +0 -0
  163. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/entry_points.txt +0 -0
  164. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/licenses/COPYING +0 -0
  165. {ert-17.1.7.dist-info → ert-18.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import fnmatch
5
+ import logging
6
+ import re
7
+ from collections import defaultdict
8
+ from typing import Literal
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+ import polars as pl
13
+ from pydantic import Field
14
+ from resfo_utilities import InvalidRFTError, RFTReader
15
+
16
+ from ert.substitutions import substitute_runpath_name
17
+
18
+ from .parsing import ConfigDict, ConfigKeys, ConfigValidationError, ConfigWarning
19
+ from .response_config import InvalidResponseFile, ResponseConfig, ResponseMetadata
20
+ from .responses_index import responses_index
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class RFTConfig(ResponseConfig):
26
+ type: Literal["rft"] = "rft"
27
+ name: str = "rft"
28
+ has_finalized_keys: bool = False
29
+ data_to_read: dict[str, dict[str, list[str]]] = Field(default_factory=dict)
30
+
31
+ @property
32
+ def metadata(self) -> list[ResponseMetadata]:
33
+ return [
34
+ ResponseMetadata(
35
+ response_type=self.name,
36
+ response_key=response_key,
37
+ filter_on=None,
38
+ finalized=self.has_finalized_keys,
39
+ )
40
+ for response_key in self.keys
41
+ ]
42
+
43
+ @property
44
+ def expected_input_files(self) -> list[str]:
45
+ base = self.input_files[0]
46
+ if base.upper().endswith(".DATA"):
47
+ # For backwards compatibility, it is
48
+ # allowed to give REFCASE and ECLBASE both
49
+ # with and without .DATA extensions
50
+ base = base[:-5]
51
+
52
+ return [f"{base}.RFT"]
53
+
54
+ def read_from_file(self, run_path: str, iens: int, iter_: int) -> pl.DataFrame:
55
+ filename = substitute_runpath_name(self.input_files[0], iens, iter_)
56
+ if filename.upper().endswith(".DATA"):
57
+ # For backwards compatibility, it is
58
+ # allowed to give REFCASE and ECLBASE both
59
+ # with and without .DATA extensions
60
+ filename = filename[:-5]
61
+ fetched: dict[tuple[str, datetime.date], dict[str, npt.NDArray[np.float32]]] = (
62
+ defaultdict(dict)
63
+ )
64
+ # This is a somewhat complicated optimization in order to
65
+ # support wildcards in well names, dates and properties
66
+ # A python for loop is too slow so we use a compiled regex
67
+ # instead
68
+ if not self.data_to_read:
69
+ return pl.DataFrame(
70
+ {
71
+ "response_key": [],
72
+ "time": [],
73
+ "depth": [],
74
+ "values": [],
75
+ }
76
+ )
77
+
78
+ sep = "\x31"
79
+
80
+ def _translate(pat: str) -> str:
81
+ """Translates fnmatch pattern to match anywhere"""
82
+ return fnmatch.translate(pat).replace("\\z", "").replace("\\Z", "")
83
+
84
+ def _props_matcher(props: list[str]) -> str:
85
+ """Regex for matching given props _and_ DEPTH"""
86
+ pattern = f"({'|'.join(_translate(p) for p in props)})"
87
+ if re.fullmatch(pattern, "DEPTH") is None:
88
+ return f"({'|'.join(_translate(p) for p in [*props, 'DEPTH'])})"
89
+ else:
90
+ return pattern
91
+
92
+ matcher = re.compile(
93
+ "|".join(
94
+ "("
95
+ + re.escape(sep).join(
96
+ (
97
+ _translate(well),
98
+ _translate(time),
99
+ _props_matcher(props),
100
+ )
101
+ )
102
+ + ")"
103
+ for well, inner_dict in self.data_to_read.items()
104
+ for time, props in inner_dict.items()
105
+ )
106
+ )
107
+ try:
108
+ with RFTReader.open(f"{run_path}/{filename}") as rft:
109
+ for entry in rft:
110
+ date = entry.date
111
+ well = entry.well
112
+ for rft_property in entry:
113
+ key = f"{well}{sep}{date}{sep}{rft_property}"
114
+ if matcher.fullmatch(key) is not None:
115
+ values = entry[rft_property]
116
+ if np.isdtype(values.dtype, np.float32):
117
+ fetched[well, date][rft_property] = values
118
+ except (FileNotFoundError, InvalidRFTError) as err:
119
+ raise InvalidResponseFile(
120
+ f"Could not read RFT from {run_path}/{filename}: {err}"
121
+ ) from err
122
+
123
+ if not fetched:
124
+ return pl.DataFrame(
125
+ {
126
+ "response_key": [],
127
+ "time": [],
128
+ "depth": [],
129
+ "values": [],
130
+ }
131
+ )
132
+
133
+ dfs = []
134
+
135
+ for (well, time), inner_dict in fetched.items():
136
+ wide = pl.DataFrame(
137
+ {k: pl.Series(v.astype("<f4")) for k, v in inner_dict.items()}
138
+ )
139
+
140
+ if wide.columns == ["DEPTH"]:
141
+ continue
142
+
143
+ if "DEPTH" not in wide.columns:
144
+ raise InvalidResponseFile(f"Could not find DEPTH in RFTFile {filename}")
145
+
146
+ # Unpivot all columns except DEPTH
147
+ long = wide.unpivot(
148
+ index="DEPTH", # keep depth as column
149
+ # turn other prop values into response_key col
150
+ variable_name="response_key",
151
+ value_name="values", # put values in own column
152
+ ).rename({"DEPTH": "depth"})
153
+
154
+ # Add wellname prefix to response_keys
155
+ long = long.with_columns(
156
+ (pl.lit(f"{well}:{time.isoformat()}:") + pl.col("response_key")).alias(
157
+ "response_key"
158
+ ),
159
+ pl.lit(time).alias("time"),
160
+ )
161
+
162
+ dfs.append(long.select("response_key", "time", "depth", "values"))
163
+
164
+ return pl.concat(dfs)
165
+
166
+ @property
167
+ def response_type(self) -> str:
168
+ return "rft"
169
+
170
+ @property
171
+ def primary_key(self) -> list[str]:
172
+ return []
173
+
174
+ @classmethod
175
+ def from_config_dict(cls, config_dict: ConfigDict) -> RFTConfig | None:
176
+ if rfts := config_dict.get(ConfigKeys.RFT, []):
177
+ eclbase: str | None = config_dict.get("ECLBASE")
178
+ if eclbase is None:
179
+ raise ConfigValidationError(
180
+ "In order to use rft responses, ECLBASE has to be set."
181
+ )
182
+ fm_steps = config_dict.get(ConfigKeys.FORWARD_MODEL, [])
183
+ names = [fm_step[0] for fm_step in fm_steps]
184
+ simulation_step_exists = any(
185
+ any(sim in name.lower() for sim in ["eclipse", "flow"])
186
+ for name in names
187
+ )
188
+ if not simulation_step_exists:
189
+ ConfigWarning.warn(
190
+ "Config contains a RFT key but no forward model "
191
+ "step known to generate rft files"
192
+ )
193
+
194
+ declared_data: dict[str, dict[datetime.date, list[str]]] = defaultdict(
195
+ lambda: defaultdict(list)
196
+ )
197
+ for rft in rfts:
198
+ for expected in ["WELL", "DATE", "PROPERTIES"]:
199
+ if expected not in rft:
200
+ raise ConfigValidationError.with_context(
201
+ f"For RFT keyword {expected} must be specified.", rft
202
+ )
203
+ well = rft["WELL"]
204
+ props = [p.strip() for p in rft["PROPERTIES"].split(",")]
205
+ time = rft["DATE"]
206
+ declared_data[well][time] += props
207
+ data_to_read = {
208
+ well: {time: sorted(set(p)) for time, p in inner_dict.items()}
209
+ for well, inner_dict in declared_data.items()
210
+ }
211
+ keys = sorted(
212
+ {
213
+ f"{well}:{time}:{p}"
214
+ for well, inner_dict in declared_data.items()
215
+ for time, props in inner_dict.items()
216
+ for p in props
217
+ }
218
+ )
219
+
220
+ return cls(
221
+ name="rft",
222
+ input_files=[eclbase.replace("%d", "<IENS>")],
223
+ keys=keys,
224
+ data_to_read=data_to_read,
225
+ )
226
+
227
+ return None
228
+
229
+
230
+ responses_index.add_response_type(RFTConfig)
@@ -72,10 +72,6 @@ class SummaryConfig(ResponseConfig):
72
72
  df = df.sort(by=["time"])
73
73
  return df
74
74
 
75
- @property
76
- def response_type(self) -> str:
77
- return "summary"
78
-
79
75
  @property
80
76
  def primary_key(self) -> list[str]:
81
77
  return ["time"]
@@ -91,8 +87,8 @@ class SummaryConfig(ResponseConfig):
91
87
  fm_steps = config_dict.get(ConfigKeys.FORWARD_MODEL, [])
92
88
  names = [fm_step[0] for fm_step in fm_steps]
93
89
  simulation_step_exists = any(
94
- any(sim in _name.lower() for sim in ["eclipse", "flow"])
95
- for _name in names
90
+ any(sim in name.lower() for sim in ["eclipse", "flow"])
91
+ for name in names
96
92
  )
97
93
  if not simulation_step_exists:
98
94
  ConfigWarning.warn(
File without changes
@@ -6,12 +6,13 @@ import typing
6
6
  from dataclasses import dataclass, fields
7
7
  from typing import TYPE_CHECKING, Literal
8
8
 
9
- from PyQt6.QtWidgets import QWidget
10
9
  from typing_extensions import TypedDict
11
10
 
12
11
  from ert.config.parsing.hook_runtime import HookRuntime
13
12
 
14
13
  if TYPE_CHECKING:
14
+ from PyQt6.QtWidgets import QWidget
15
+
15
16
  from ert.config import ESSettings, ObservationSettings
16
17
  from ert.runpaths import Runpaths
17
18
  from ert.storage import Ensemble, Storage
@@ -1,4 +1,4 @@
1
- from ._session import ConnInfo
1
+ from ._session import ErtClientConnectionInfo
2
2
  from .client import Client
3
3
 
4
- __all__ = ["Client", "ConnInfo"]
4
+ __all__ = ["Client", "ErtClientConnectionInfo"]
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from pydantic import BaseModel, ValidationError
6
6
 
7
7
 
8
- class ConnInfo(BaseModel):
8
+ class ErtClientConnectionInfo(BaseModel):
9
9
  base_url: str
10
10
  auth_token: str | None = None
11
11
  cert: str | bool = False
@@ -17,10 +17,10 @@ ENV_VAR = "ERT_STORAGE_CONNECTION_STRING"
17
17
  # that a single client process will only ever want to connect to a single ERT
18
18
  # Storage server during its lifetime, so we don't provide an API for managing
19
19
  # this cache.
20
- _CACHED_CONN_INFO: ConnInfo | None = None
20
+ _CACHED_CONN_INFO: ErtClientConnectionInfo | None = None
21
21
 
22
22
 
23
- def find_conn_info() -> ConnInfo:
23
+ def find_conn_info() -> ErtClientConnectionInfo:
24
24
  """
25
25
  The base url and auth token are read from either:
26
26
  The file `storage_server.json`, starting from the current working directory
@@ -54,7 +54,7 @@ def find_conn_info() -> ConnInfo:
54
54
  raise RuntimeError("No Storage connection configuration found")
55
55
 
56
56
  try:
57
- conn_info = ConnInfo.model_validate_json(conn_str)
57
+ conn_info = ErtClientConnectionInfo.model_validate_json(conn_str)
58
58
  except (json.JSONDecodeError, ValidationError) as e:
59
59
  raise RuntimeError("Invalid storage connection configuration") from e
60
60
  else:
@@ -3,7 +3,7 @@ import ssl
3
3
  import httpx
4
4
  from httpx_retries import Retry, RetryTransport
5
5
 
6
- from ._session import ConnInfo, find_conn_info
6
+ from ._session import ErtClientConnectionInfo, find_conn_info
7
7
 
8
8
 
9
9
  class Client(httpx.Client):
@@ -14,7 +14,7 @@ class Client(httpx.Client):
14
14
  Stores 'conn_info' to bridge the gap to the Everest client setup
15
15
  """
16
16
 
17
- def __init__(self, conn_info: ConnInfo | None = None) -> None:
17
+ def __init__(self, conn_info: ErtClientConnectionInfo | None = None) -> None:
18
18
  if conn_info is None:
19
19
  conn_info = find_conn_info()
20
20
 
@@ -5,17 +5,18 @@ import numpy.typing as npt
5
5
  import pandas as pd
6
6
 
7
7
 
8
- def _calculate_misfit(
8
+ def _calculate_signed_chi_squared_misfit(
9
9
  obs_value: npt.NDArray[np.float64],
10
10
  response_value: npt.NDArray[np.float64],
11
11
  obs_std: npt.NDArray[np.float64],
12
12
  ) -> list[float]:
13
- difference = response_value - obs_value
14
- misfit = (difference / obs_std) ** 2
15
- return (misfit * np.sign(difference)).tolist()
13
+ """The signed version is intended for visualization. For data assimiliation one
14
+ would normally use the normal chi-square"""
15
+ residual = response_value - obs_value
16
+ return (np.sign(residual) * residual * residual / (obs_std * obs_std)).tolist()
16
17
 
17
18
 
18
- def calculate_misfits_from_pandas(
19
+ def calculate_signed_chi_squared_misfits(
19
20
  reponses_dict: Mapping[int, pd.DataFrame],
20
21
  observation: pd.DataFrame,
21
22
  summary_misfits: bool = False,
@@ -26,7 +27,7 @@ def calculate_misfits_from_pandas(
26
27
  """
27
28
  misfits_dict = {}
28
29
  for realization_index in reponses_dict:
29
- misfits_dict[realization_index] = _calculate_misfit(
30
+ misfits_dict[realization_index] = _calculate_signed_chi_squared_misfit(
30
31
  observation["values"],
31
32
  reponses_dict[realization_index].loc[:, observation.index].values.flatten(),
32
33
  observation["errors"],
@@ -10,7 +10,7 @@ from fastapi.responses import Response
10
10
 
11
11
  from ert.dark_storage import exceptions as exc
12
12
  from ert.dark_storage.common import get_storage
13
- from ert.dark_storage.compute.misfits import calculate_misfits_from_pandas
13
+ from ert.dark_storage.compute.misfits import calculate_signed_chi_squared_misfits
14
14
  from ert.dark_storage.endpoints.observations import (
15
15
  _get_observations,
16
16
  )
@@ -80,7 +80,7 @@ async def get_response_misfits(
80
80
  index=[parse_index(x) for x in o["x_axis"]],
81
81
  )
82
82
  try:
83
- result_df = calculate_misfits_from_pandas(
83
+ result_df = calculate_signed_chi_squared_misfits(
84
84
  response_dict, observation_df, summary_misfits
85
85
  )
86
86
  except Exception as misfits_exc:
@@ -136,13 +136,13 @@ def _get_observations(
136
136
  df = df.with_columns(pl.Series(name="x_axis", values=df.map_rows(x_axis_fn)))
137
137
  df = df.sort("x_axis")
138
138
 
139
- for obs_key, _obs_df in df.group_by("name"):
139
+ for obs_key, obs_df in df.group_by("name"):
140
140
  observations.append(
141
141
  {
142
142
  "name": obs_key[0],
143
- "values": _obs_df["values"].to_list(),
144
- "errors": _obs_df["errors"].to_list(),
145
- "x_axis": _obs_df["x_axis"].to_list(),
143
+ "values": obs_df["values"].to_list(),
144
+ "errors": obs_df["errors"].to_list(),
145
+ "x_axis": obs_df["x_axis"].to_list(),
146
146
  }
147
147
  )
148
148
 
@@ -116,7 +116,7 @@ def _extract_response_type_and_key(
116
116
 
117
117
  def data_for_response(
118
118
  ensemble: Ensemble, key: str, filter_on: dict[str, Any] | None = None
119
- ) -> pd.DataFrame:
119
+ ) -> pd.DataFrame | pd.Series:
120
120
  response_key, response_type = _extract_response_type_and_key(
121
121
  key, ensemble.experiment.response_key_to_response_type
122
122
  )
@@ -151,6 +151,19 @@ def data_for_response(
151
151
  data.columns = data.columns.droplevel(0)
152
152
  return data.astype(float)
153
153
 
154
+ if response_type == "rft":
155
+ return (
156
+ ensemble.load_responses(
157
+ response_key,
158
+ tuple(realizations_with_responses),
159
+ )
160
+ .rename({"realization": "Realization"})
161
+ .select(["Realization", "depth", "values"])
162
+ .to_pandas()
163
+ .pivot(index="Realization", columns="depth", values="values")
164
+ .reset_index(drop=True)
165
+ )
166
+
154
167
  if response_type == "gen_data":
155
168
  data = ensemble.load_responses(response_key, tuple(realizations_with_responses))
156
169
 
@@ -169,3 +182,4 @@ def data_for_response(
169
182
 
170
183
  except (ValueError, KeyError, ColumnNotFoundError):
171
184
  return pd.DataFrame()
185
+ return pd.DataFrame()
@@ -2,7 +2,13 @@ from ._ensemble import LegacyEnsemble as Ensemble
2
2
  from ._ensemble import Realization
3
3
  from .config import EvaluatorServerConfig
4
4
  from .evaluator import EnsembleEvaluator
5
- from .event import EndEvent, FullSnapshotEvent, SnapshotUpdateEvent, WarningEvent
5
+ from .event import (
6
+ EndEvent,
7
+ FullSnapshotEvent,
8
+ SnapshotUpdateEvent,
9
+ StartEvent,
10
+ WarningEvent,
11
+ )
6
12
  from .snapshot import EnsembleSnapshot, FMStepSnapshot, RealizationSnapshot
7
13
 
8
14
  __all__ = [
@@ -16,5 +22,6 @@ __all__ = [
16
22
  "Realization",
17
23
  "RealizationSnapshot",
18
24
  "SnapshotUpdateEvent",
25
+ "StartEvent",
19
26
  "WarningEvent",
20
27
  ]
@@ -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"