ert 17.1.9__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.
- _ert/events.py +19 -2
- ert/__main__.py +8 -7
- ert/analysis/_update_commons.py +12 -3
- ert/cli/main.py +6 -3
- ert/cli/monitor.py +7 -0
- ert/config/__init__.py +13 -3
- ert/config/_create_observation_dataframes.py +60 -12
- ert/config/_observations.py +14 -1
- ert/config/_read_summary.py +8 -6
- ert/config/ensemble_config.py +6 -14
- ert/config/ert_config.py +19 -13
- ert/config/{everest_objective_config.py → everest_response.py} +23 -12
- ert/config/ext_param_config.py +133 -1
- ert/config/field.py +12 -8
- ert/config/forward_model_step.py +108 -6
- ert/config/gen_data_config.py +2 -6
- ert/config/gen_kw_config.py +0 -9
- ert/config/known_response_types.py +14 -0
- ert/config/parameter_config.py +0 -17
- ert/config/parsing/config_keywords.py +1 -0
- ert/config/parsing/config_schema.py +12 -0
- ert/config/parsing/config_schema_deprecations.py +11 -0
- ert/config/parsing/config_schema_item.py +1 -1
- ert/config/queue_config.py +4 -4
- ert/config/response_config.py +0 -7
- ert/config/rft_config.py +230 -0
- ert/config/summary_config.py +2 -6
- ert/config/violations.py +0 -0
- ert/config/workflow_fixtures.py +2 -1
- ert/dark_storage/client/__init__.py +2 -2
- ert/dark_storage/client/_session.py +4 -4
- ert/dark_storage/client/client.py +2 -2
- ert/dark_storage/compute/misfits.py +7 -6
- ert/dark_storage/endpoints/compute/misfits.py +2 -2
- ert/dark_storage/endpoints/observations.py +4 -4
- ert/dark_storage/endpoints/responses.py +15 -1
- ert/ensemble_evaluator/__init__.py +8 -1
- ert/ensemble_evaluator/evaluator.py +81 -29
- ert/ensemble_evaluator/event.py +6 -0
- ert/ensemble_evaluator/snapshot.py +3 -1
- ert/ensemble_evaluator/state.py +1 -0
- ert/field_utils/__init__.py +8 -0
- ert/field_utils/field_utils.py +211 -1
- ert/gui/ertwidgets/__init__.py +23 -16
- ert/gui/ertwidgets/analysismoduleedit.py +2 -2
- ert/gui/ertwidgets/checklist.py +1 -1
- ert/gui/ertwidgets/create_experiment_dialog.py +3 -1
- ert/gui/ertwidgets/ensembleselector.py +2 -2
- ert/gui/ertwidgets/models/__init__.py +2 -0
- ert/gui/ertwidgets/models/activerealizationsmodel.py +2 -1
- ert/gui/ertwidgets/models/path_model.py +1 -1
- ert/gui/ertwidgets/models/targetensemblemodel.py +2 -1
- ert/gui/ertwidgets/models/text_model.py +1 -1
- ert/gui/ertwidgets/searchbox.py +13 -4
- ert/gui/{suggestor → ertwidgets/suggestor}/_suggestor_message.py +13 -4
- ert/gui/main.py +11 -6
- ert/gui/main_window.py +1 -2
- ert/gui/simulation/ensemble_experiment_panel.py +1 -1
- ert/gui/simulation/ensemble_information_filter_panel.py +1 -1
- ert/gui/simulation/ensemble_smoother_panel.py +1 -1
- ert/gui/simulation/evaluate_ensemble_panel.py +1 -1
- ert/gui/simulation/experiment_panel.py +1 -1
- ert/gui/simulation/manual_update_panel.py +31 -8
- ert/gui/simulation/multiple_data_assimilation_panel.py +12 -8
- ert/gui/simulation/run_dialog.py +25 -4
- ert/gui/simulation/single_test_run_panel.py +2 -2
- ert/gui/summarypanel.py +1 -1
- ert/gui/tools/load_results/load_results_panel.py +1 -1
- ert/gui/tools/manage_experiments/storage_info_widget.py +7 -7
- ert/gui/tools/manage_experiments/storage_widget.py +1 -2
- ert/gui/tools/plot/plot_api.py +13 -10
- ert/gui/tools/plot/plot_window.py +12 -0
- ert/gui/tools/plot/plottery/plot_config.py +2 -0
- ert/gui/tools/plot/plottery/plot_context.py +14 -0
- ert/gui/tools/plot/plottery/plots/ensemble.py +9 -2
- ert/gui/tools/plot/plottery/plots/statistics.py +59 -19
- ert/mode_definitions.py +2 -0
- ert/plugins/__init__.py +0 -1
- ert/plugins/hook_implementations/workflows/gen_data_rft_export.py +10 -2
- ert/plugins/hook_specifications/__init__.py +0 -2
- ert/plugins/hook_specifications/jobs.py +0 -9
- ert/plugins/plugin_manager.py +2 -33
- ert/resources/shell_scripts/delete_directory.py +2 -2
- ert/run_models/__init__.py +18 -5
- ert/run_models/_create_run_path.py +33 -21
- ert/run_models/ensemble_experiment.py +10 -4
- ert/run_models/ensemble_information_filter.py +8 -1
- ert/run_models/ensemble_smoother.py +9 -3
- ert/run_models/evaluate_ensemble.py +8 -6
- ert/run_models/event.py +7 -3
- ert/run_models/everest_run_model.py +155 -44
- ert/run_models/initial_ensemble_run_model.py +23 -22
- ert/run_models/manual_update.py +4 -2
- ert/run_models/manual_update_enif.py +37 -0
- ert/run_models/model_factory.py +81 -22
- ert/run_models/multiple_data_assimilation.py +21 -10
- ert/run_models/run_model.py +54 -34
- ert/run_models/single_test_run.py +7 -4
- ert/run_models/update_run_model.py +4 -2
- ert/runpaths.py +5 -6
- ert/sample_prior.py +9 -4
- ert/scheduler/driver.py +37 -0
- ert/scheduler/event.py +3 -1
- ert/scheduler/job.py +23 -13
- ert/scheduler/lsf_driver.py +6 -2
- ert/scheduler/openpbs_driver.py +7 -1
- ert/scheduler/scheduler.py +5 -0
- ert/scheduler/slurm_driver.py +6 -2
- ert/services/__init__.py +2 -2
- ert/services/_base_service.py +31 -15
- ert/services/ert_server.py +317 -0
- ert/shared/_doc_utils/ert_jobs.py +1 -4
- ert/shared/storage/connection.py +3 -3
- ert/shared/version.py +3 -3
- ert/storage/local_ensemble.py +25 -5
- ert/storage/local_experiment.py +6 -14
- ert/storage/local_storage.py +35 -30
- ert/storage/migration/to18.py +12 -0
- ert/storage/migration/to8.py +4 -4
- ert/substitutions.py +12 -28
- ert/validation/active_range.py +7 -7
- ert/validation/rangestring.py +16 -16
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/METADATA +8 -7
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/RECORD +160 -159
- everest/api/everest_data_api.py +1 -14
- everest/bin/config_branch_script.py +3 -6
- everest/bin/everconfigdump_script.py +1 -9
- everest/bin/everest_script.py +21 -11
- everest/bin/kill_script.py +2 -2
- everest/bin/monitor_script.py +2 -2
- everest/bin/utils.py +6 -3
- everest/config/__init__.py +4 -1
- everest/config/control_config.py +61 -2
- everest/config/control_variable_config.py +2 -1
- everest/config/everest_config.py +38 -16
- everest/config/forward_model_config.py +5 -3
- everest/config/install_data_config.py +7 -5
- everest/config/install_job_config.py +7 -3
- everest/config/install_template_config.py +3 -3
- everest/config/optimization_config.py +19 -6
- everest/config/output_constraint_config.py +8 -2
- everest/config/server_config.py +6 -49
- everest/config/utils.py +25 -105
- everest/config/validation_utils.py +10 -10
- everest/config_file_loader.py +13 -2
- everest/detached/everserver.py +7 -8
- everest/everest_storage.py +6 -10
- everest/gui/everest_client.py +0 -1
- everest/gui/main_window.py +2 -2
- everest/optimizer/everest2ropt.py +59 -32
- everest/optimizer/opt_model_transforms.py +12 -13
- everest/optimizer/utils.py +0 -29
- everest/strings.py +0 -5
- ert/config/everest_constraints_config.py +0 -95
- ert/services/storage_service.py +0 -127
- everest/config/sampler_config.py +0 -103
- everest/simulator/__init__.py +0 -88
- everest/simulator/everest_to_ert.py +0 -51
- /ert/gui/{suggestor → ertwidgets/suggestor}/__init__.py +0 -0
- /ert/gui/{suggestor → ertwidgets/suggestor}/_colors.py +0 -0
- /ert/gui/{suggestor → ertwidgets/suggestor}/suggestor.py +0 -0
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/WHEEL +0 -0
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/entry_points.txt +0 -0
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/licenses/COPYING +0 -0
- {ert-17.1.9.dist-info → ert-18.0.0.dist-info}/top_level.txt +0 -0
ert/config/rft_config.py
ADDED
|
@@ -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)
|
ert/config/summary_config.py
CHANGED
|
@@ -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
|
|
95
|
-
for
|
|
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(
|
ert/config/violations.py
ADDED
|
File without changes
|
ert/config/workflow_fixtures.py
CHANGED
|
@@ -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
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic import BaseModel, ValidationError
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
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:
|
|
20
|
+
_CACHED_CONN_INFO: ErtClientConnectionInfo | None = None
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def find_conn_info() ->
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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] =
|
|
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
|
|
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 =
|
|
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,
|
|
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":
|
|
144
|
-
"errors":
|
|
145
|
-
"x_axis":
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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
|
+
)
|
ert/ensemble_evaluator/event.py
CHANGED
|
@@ -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"
|