ert 19.0.1__py3-none-any.whl → 20.0.0b1__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/__main__.py +94 -63
- ert/analysis/_es_update.py +11 -14
- ert/cli/main.py +1 -1
- ert/config/__init__.py +3 -2
- ert/config/_create_observation_dataframes.py +52 -375
- ert/config/_observations.py +527 -200
- ert/config/_read_summary.py +4 -5
- ert/config/ert_config.py +52 -117
- ert/config/everest_control.py +40 -39
- ert/config/everest_response.py +3 -15
- ert/config/field.py +4 -76
- ert/config/forward_model_step.py +17 -1
- ert/config/gen_data_config.py +14 -17
- ert/config/observation_config_migrations.py +821 -0
- ert/config/parameter_config.py +18 -28
- ert/config/parsing/__init__.py +0 -1
- ert/config/parsing/_parse_zonemap.py +45 -0
- ert/config/parsing/config_keywords.py +1 -0
- ert/config/parsing/config_schema.py +2 -0
- ert/config/parsing/observations_parser.py +2 -0
- ert/config/response_config.py +5 -23
- ert/config/rft_config.py +129 -31
- ert/config/summary_config.py +1 -13
- ert/config/surface_config.py +0 -57
- ert/dark_storage/compute/misfits.py +0 -42
- ert/dark_storage/endpoints/__init__.py +0 -2
- ert/dark_storage/endpoints/experiments.py +2 -5
- ert/dark_storage/json_schema/experiment.py +1 -2
- ert/field_utils/__init__.py +0 -2
- ert/field_utils/field_utils.py +1 -117
- ert/gui/ertwidgets/listeditbox.py +9 -1
- ert/gui/ertwidgets/models/ertsummary.py +20 -6
- ert/gui/ertwidgets/pathchooser.py +9 -1
- ert/gui/ertwidgets/stringbox.py +11 -3
- ert/gui/ertwidgets/textbox.py +10 -3
- ert/gui/ertwidgets/validationsupport.py +19 -1
- ert/gui/main_window.py +11 -6
- ert/gui/simulation/experiment_panel.py +1 -1
- ert/gui/simulation/run_dialog.py +11 -1
- ert/gui/tools/manage_experiments/export_dialog.py +4 -0
- ert/gui/tools/manage_experiments/manage_experiments_panel.py +1 -0
- ert/gui/tools/manage_experiments/storage_info_widget.py +1 -1
- ert/gui/tools/manage_experiments/storage_widget.py +21 -4
- ert/gui/tools/plot/data_type_proxy_model.py +1 -1
- ert/gui/tools/plot/plot_api.py +35 -27
- ert/gui/tools/plot/plot_widget.py +5 -0
- ert/gui/tools/plot/plot_window.py +4 -7
- ert/run_models/ensemble_experiment.py +2 -9
- ert/run_models/ensemble_smoother.py +1 -9
- ert/run_models/everest_run_model.py +31 -23
- ert/run_models/initial_ensemble_run_model.py +19 -22
- ert/run_models/manual_update.py +11 -5
- ert/run_models/model_factory.py +7 -7
- ert/run_models/multiple_data_assimilation.py +3 -16
- ert/sample_prior.py +12 -14
- ert/scheduler/job.py +24 -4
- ert/services/__init__.py +7 -3
- ert/services/_storage_main.py +59 -22
- ert/services/ert_server.py +186 -24
- ert/shared/version.py +3 -3
- ert/storage/local_ensemble.py +50 -116
- ert/storage/local_experiment.py +94 -109
- ert/storage/local_storage.py +10 -12
- ert/storage/migration/to24.py +26 -0
- ert/storage/migration/to25.py +91 -0
- ert/utils/__init__.py +20 -0
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/METADATA +4 -51
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/RECORD +80 -83
- everest/bin/everest_script.py +5 -5
- everest/bin/kill_script.py +2 -2
- everest/bin/monitor_script.py +2 -2
- everest/bin/utils.py +4 -4
- everest/detached/everserver.py +6 -6
- everest/gui/everest_client.py +0 -6
- everest/gui/main_window.py +2 -2
- everest/util/__init__.py +1 -19
- ert/dark_storage/compute/__init__.py +0 -0
- ert/dark_storage/endpoints/compute/__init__.py +0 -0
- ert/dark_storage/endpoints/compute/misfits.py +0 -95
- ert/services/_base_service.py +0 -387
- ert/services/webviz_ert_service.py +0 -20
- ert/shared/storage/command.py +0 -38
- ert/shared/storage/extraction.py +0 -42
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/WHEEL +0 -0
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/entry_points.txt +0 -0
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/licenses/COPYING +0 -0
- {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/top_level.txt +0 -0
ert/config/parameter_config.py
CHANGED
|
@@ -126,35 +126,28 @@ class ParameterConfig(BaseModel):
|
|
|
126
126
|
def transform_data(self) -> Callable[[float], float]:
|
|
127
127
|
return lambda x: x
|
|
128
128
|
|
|
129
|
-
def
|
|
130
|
-
self,
|
|
131
|
-
global_seed: str,
|
|
132
|
-
realization: int,
|
|
129
|
+
def sample_values(
|
|
130
|
+
self, global_seed: str, active_realizations: list[int], num_realizations: int
|
|
133
131
|
) -> npt.NDArray[np.double]:
|
|
134
132
|
"""
|
|
135
|
-
Generate
|
|
133
|
+
Generate reproducible standard-normal samples for active realizations.
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
For this parameter (identified by self.group_name and self.name), a random
|
|
136
|
+
sampling of size `num_realizations` is constructed using an RNG
|
|
137
|
+
seeded from `global_seed` and the parameter name. The entries at the
|
|
138
|
+
indices specified by `active_realizations` are then returned.
|
|
141
139
|
|
|
142
140
|
Parameters:
|
|
143
|
-
- global_seed (str): A global seed string used for RNG seed generation to
|
|
144
|
-
reproducibility across runs.
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
- global_seed (str): A global seed string used for RNG seed generation to
|
|
142
|
+
ensure reproducibility across runs.
|
|
143
|
+
- active_realizations (list[int]): indices of the realizations
|
|
144
|
+
to select from the sampling vector; each must satisfy 0 <= i < num_realizations.
|
|
145
|
+
- num_realizations (int): Total number of realizations. Assures stable sampling
|
|
146
|
+
for a given global_seed regardless of currently active realizations.
|
|
148
147
|
|
|
149
148
|
Returns:
|
|
150
|
-
- npt.NDArray[np.double]:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Note:
|
|
154
|
-
The method uses SHA-256 for hash generation and numpy's default random number
|
|
155
|
-
generator for sampling. The RNG state is advanced to the 'realization' point
|
|
156
|
-
before generating a single sample, enhancing efficiency by avoiding the
|
|
157
|
-
generation of large, unused sample sets.
|
|
149
|
+
- npt.NDArray[np.double]: Array of shape (len(active_realizations),
|
|
150
|
+
containing sample values, one for each `active_realization`.
|
|
158
151
|
"""
|
|
159
152
|
key_hash = sha256(
|
|
160
153
|
global_seed.encode("utf-8") + f"{self.group_name}:{self.name}".encode()
|
|
@@ -162,9 +155,6 @@ class ParameterConfig(BaseModel):
|
|
|
162
155
|
seed = np.frombuffer(key_hash.digest(), dtype="uint32")
|
|
163
156
|
rng = np.random.default_rng(seed)
|
|
164
157
|
|
|
165
|
-
#
|
|
166
|
-
rng.standard_normal(
|
|
167
|
-
|
|
168
|
-
# Generate a single sample
|
|
169
|
-
value = rng.standard_normal(1)
|
|
170
|
-
return np.array([value[0]])
|
|
158
|
+
# Generate samples for all active realizations
|
|
159
|
+
all_values = rng.standard_normal(num_realizations)
|
|
160
|
+
return all_values[active_realizations]
|
ert/config/parsing/__init__.py
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .config_errors import ConfigValidationError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_zonemap(filename: str, content: str) -> dict[int, list[str]]:
|
|
5
|
+
"""A zonemap is a map from a simulation grids layers to a list of zone names
|
|
6
|
+
that the k-layers belongs to.
|
|
7
|
+
|
|
8
|
+
Note that the map uses 1-indexing of layers.
|
|
9
|
+
"""
|
|
10
|
+
zones_at_k_value: dict[int, list[str]] = {}
|
|
11
|
+
|
|
12
|
+
base_err_msg = "On Line {line_number} in the ZONEMAP file: "
|
|
13
|
+
for idx, line in enumerate(content.splitlines()):
|
|
14
|
+
line_number = idx + 1
|
|
15
|
+
zonemap_line = _strip_comments(line).split()
|
|
16
|
+
|
|
17
|
+
if not zonemap_line:
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
if len(zonemap_line) < 2:
|
|
21
|
+
raise ConfigValidationError.with_context(
|
|
22
|
+
"Number of zonenames must be 1 or more.",
|
|
23
|
+
filename,
|
|
24
|
+
)
|
|
25
|
+
try:
|
|
26
|
+
k = int(zonemap_line[0])
|
|
27
|
+
except ValueError as err:
|
|
28
|
+
raise ConfigValidationError.with_context(
|
|
29
|
+
base_err_msg.format(line_number=line_number)
|
|
30
|
+
+ f"k must be an integer, was {zonemap_line[0]}.",
|
|
31
|
+
filename,
|
|
32
|
+
) from err
|
|
33
|
+
if k <= 0:
|
|
34
|
+
raise ConfigValidationError.with_context(
|
|
35
|
+
base_err_msg.format(line_number=line_number)
|
|
36
|
+
+ "k must be at least 1. Layers are 1-indexed.",
|
|
37
|
+
filename,
|
|
38
|
+
)
|
|
39
|
+
zones_at_k_value[k] = [zone.strip() for zone in zonemap_line[1:]]
|
|
40
|
+
|
|
41
|
+
return zones_at_k_value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _strip_comments(line: str) -> str:
|
|
45
|
+
return line.partition("--")[0].rstrip()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ._parse_zonemap import parse_zonemap
|
|
1
2
|
from .config_dict import ConfigDict
|
|
2
3
|
from .config_keywords import ConfigKeys
|
|
3
4
|
from .config_schema_deprecations import deprecated_keywords_list
|
|
@@ -333,6 +334,7 @@ def init_user_config_schema() -> ConfigSchemaDict:
|
|
|
333
334
|
existing_path_inline_keyword(
|
|
334
335
|
ConfigKeys.OBS_CONFIG, parser=lambda f, c: parse_observations(c, f)
|
|
335
336
|
),
|
|
337
|
+
existing_path_inline_keyword(ConfigKeys.ZONEMAP, parser=parse_zonemap),
|
|
336
338
|
existing_path_inline_keyword(ConfigKeys.TIME_MAP),
|
|
337
339
|
single_arg_keyword(ConfigKeys.GEN_KW_EXPORT_NAME),
|
|
338
340
|
history_source_keyword(),
|
|
@@ -19,6 +19,7 @@ class ObservationType(StrEnum):
|
|
|
19
19
|
SUMMARY = "SUMMARY_OBSERVATION"
|
|
20
20
|
GENERAL = "GENERAL_OBSERVATION"
|
|
21
21
|
RFT = "RFT_OBSERVATION"
|
|
22
|
+
BREAKTHROUGH = "BREAKTHROUGH_OBSERVATION"
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
ObservationDict = dict[str, Any]
|
|
@@ -127,6 +128,7 @@ observations_parser = Lark(
|
|
|
127
128
|
| "SUMMARY_OBSERVATION"
|
|
128
129
|
| "GENERAL_OBSERVATION"
|
|
129
130
|
| "RFT_OBSERVATION"
|
|
131
|
+
| "BREAKTHROUGH_OBSERVATION"
|
|
130
132
|
type: TYPE
|
|
131
133
|
?value: object
|
|
132
134
|
| STRING
|
ert/config/response_config.py
CHANGED
|
@@ -14,35 +14,12 @@ class InvalidResponseFile(Exception):
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class ResponseMetadata(BaseModel):
|
|
18
|
-
response_type: str
|
|
19
|
-
response_key: str
|
|
20
|
-
finalized: bool
|
|
21
|
-
filter_on: dict[str, list[Any]] | None = Field(
|
|
22
|
-
default=None,
|
|
23
|
-
description="""
|
|
24
|
-
Holds information about which columns the response can be filtered on.
|
|
25
|
-
For example, for gen data, { "report_step": [0, 199, 299] } indicates
|
|
26
|
-
that we can filter the response by report step with the potential values
|
|
27
|
-
[0, 199, 299].
|
|
28
|
-
""",
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
17
|
class ResponseConfig(BaseModel):
|
|
33
18
|
type: str
|
|
34
19
|
input_files: list[str] = Field(default_factory=list)
|
|
35
20
|
keys: list[str] = Field(default_factory=list)
|
|
36
21
|
has_finalized_keys: bool = False
|
|
37
22
|
|
|
38
|
-
@property
|
|
39
|
-
@abstractmethod
|
|
40
|
-
def metadata(self) -> list[ResponseMetadata]:
|
|
41
|
-
"""
|
|
42
|
-
Returns metadata describing this response
|
|
43
|
-
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
23
|
@abstractmethod
|
|
47
24
|
def read_from_file(self, run_path: str, iens: int, iter_: int) -> pl.DataFrame:
|
|
48
25
|
"""Reads the data for the response from run_path.
|
|
@@ -76,3 +53,8 @@ class ResponseConfig(BaseModel):
|
|
|
76
53
|
def display_column(cls, value: Any, column_name: str) -> str:
|
|
77
54
|
"""Formats a value to a user-friendly displayable format."""
|
|
78
55
|
return str(value)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def filter_on(self) -> dict[str, dict[str, list[int]]] | None:
|
|
59
|
+
"""Optional filters for this response."""
|
|
60
|
+
return None
|
ert/config/rft_config.py
CHANGED
|
@@ -5,8 +5,10 @@ import fnmatch
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
+
import warnings
|
|
8
9
|
from collections import defaultdict
|
|
9
|
-
from
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import IO, Any, Literal, TypeAlias, cast
|
|
10
12
|
|
|
11
13
|
import numpy as np
|
|
12
14
|
import numpy.typing as npt
|
|
@@ -15,31 +17,67 @@ from pydantic import Field
|
|
|
15
17
|
from resfo_utilities import CornerpointGrid, InvalidRFTError, RFTReader
|
|
16
18
|
|
|
17
19
|
from ert.substitutions import substitute_runpath_name
|
|
20
|
+
from ert.warnings import PostSimulationWarning
|
|
18
21
|
|
|
19
22
|
from .parsing import ConfigDict, ConfigKeys, ConfigValidationError, ConfigWarning
|
|
20
|
-
from .response_config import InvalidResponseFile, ResponseConfig
|
|
23
|
+
from .response_config import InvalidResponseFile, ResponseConfig
|
|
21
24
|
from .responses_index import responses_index
|
|
22
25
|
|
|
23
26
|
logger = logging.getLogger(__name__)
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
# A Point in UTM/TVD coordinates
|
|
30
|
+
Point: TypeAlias = tuple[float, float, float]
|
|
31
|
+
# Index to a cell in a grid
|
|
32
|
+
GridIndex: TypeAlias = tuple[int, int, int]
|
|
33
|
+
ZoneName: TypeAlias = str
|
|
34
|
+
WellName: TypeAlias = str
|
|
35
|
+
DateString: TypeAlias = str
|
|
36
|
+
RFTProperty: TypeAlias = str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class _ZonedPoint:
|
|
41
|
+
"""A point optionally constrained to be in a given zone."""
|
|
42
|
+
|
|
43
|
+
point: tuple[float | None, float | None, float | None] = (None, None, None)
|
|
44
|
+
zone_name: ZoneName | None = None
|
|
45
|
+
|
|
46
|
+
def has_zone(self) -> bool:
|
|
47
|
+
return self.zone_name is not None
|
|
48
|
+
|
|
49
|
+
|
|
26
50
|
class RFTConfig(ResponseConfig):
|
|
51
|
+
""":term:`RFT` response from a :term:`reservoir simulator`.
|
|
52
|
+
|
|
53
|
+
RFTConfig is the configuration of responses in the <RUNPATH>/<ECLBASE>.RFT
|
|
54
|
+
file which may be generated from a reservoir simulator forward model step.
|
|
55
|
+
|
|
56
|
+
The file contains values for grid cells along a wellpath (see RFTReader for
|
|
57
|
+
details). RFTConfig will match the values against the given :term:`UTM`/:term:`TVD`
|
|
58
|
+
locations.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
data_to_read: dictionary of the values that should be read from the rft file.
|
|
62
|
+
loations: list of optionally zone constrained points that the rft values should
|
|
63
|
+
be labeled with.
|
|
64
|
+
zonemap: The mapping from grid layer index to zone name.
|
|
65
|
+
"""
|
|
66
|
+
|
|
27
67
|
type: Literal["rft"] = "rft"
|
|
28
68
|
name: str = "rft"
|
|
29
69
|
has_finalized_keys: bool = False
|
|
30
|
-
data_to_read: dict[
|
|
31
|
-
|
|
70
|
+
data_to_read: dict[WellName, dict[DateString, list[RFTProperty]]] = Field(
|
|
71
|
+
default_factory=dict
|
|
72
|
+
)
|
|
73
|
+
locations: list[Point | tuple[Point, ZoneName]] = Field(default_factory=list)
|
|
74
|
+
zonemap: dict[int, list[ZoneName]] = Field(default_factory=dict)
|
|
32
75
|
|
|
33
76
|
@property
|
|
34
|
-
def
|
|
77
|
+
def _zoned_locations(self) -> list[_ZonedPoint]:
|
|
35
78
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
response_key=response_key,
|
|
39
|
-
filter_on=None,
|
|
40
|
-
finalized=self.has_finalized_keys,
|
|
41
|
-
)
|
|
42
|
-
for response_key in self.keys
|
|
79
|
+
_ZonedPoint(*p) if isinstance(p[1], ZoneName) else _ZonedPoint(p)
|
|
80
|
+
for p in self.locations
|
|
43
81
|
]
|
|
44
82
|
|
|
45
83
|
@property
|
|
@@ -55,19 +93,56 @@ class RFTConfig(ResponseConfig):
|
|
|
55
93
|
|
|
56
94
|
def _find_indices(
|
|
57
95
|
self, egrid_file: str | os.PathLike[str] | IO[Any]
|
|
58
|
-
) -> dict[
|
|
96
|
+
) -> dict[GridIndex | None, set[_ZonedPoint]]:
|
|
59
97
|
indices = defaultdict(set)
|
|
60
98
|
for a, b in zip(
|
|
61
99
|
CornerpointGrid.read_egrid(egrid_file).find_cell_containing_point(
|
|
62
|
-
self.
|
|
100
|
+
[cast(Point, loc.point) for loc in self._zoned_locations]
|
|
63
101
|
),
|
|
64
|
-
self.
|
|
102
|
+
self._zoned_locations,
|
|
65
103
|
strict=True,
|
|
66
104
|
):
|
|
67
105
|
indices[a].add(b)
|
|
68
106
|
return indices
|
|
69
107
|
|
|
108
|
+
def _filter_zones(
|
|
109
|
+
self,
|
|
110
|
+
indices: dict[GridIndex | None, set[_ZonedPoint]],
|
|
111
|
+
iens: int,
|
|
112
|
+
iter_: int,
|
|
113
|
+
) -> dict[GridIndex | None, set[_ZonedPoint]]:
|
|
114
|
+
for idx, locs in indices.items():
|
|
115
|
+
if idx is not None:
|
|
116
|
+
for loc in list(locs):
|
|
117
|
+
if loc.has_zone():
|
|
118
|
+
zone = cast(ZoneName, loc.zone_name)
|
|
119
|
+
# zonemap is 1-indexed so +1
|
|
120
|
+
if zone not in self.zonemap.get(idx[-1] + 1, []):
|
|
121
|
+
warnings.warn(
|
|
122
|
+
PostSimulationWarning(
|
|
123
|
+
f"An RFT observation with location {loc.point}, "
|
|
124
|
+
f"in iteration {iter_}, realization {iens} did "
|
|
125
|
+
f"not match expected zone {zone}. The observation "
|
|
126
|
+
"was deactivated",
|
|
127
|
+
),
|
|
128
|
+
stacklevel=2,
|
|
129
|
+
)
|
|
130
|
+
locs.remove(loc)
|
|
131
|
+
return indices
|
|
132
|
+
|
|
70
133
|
def read_from_file(self, run_path: str, iens: int, iter_: int) -> pl.DataFrame:
|
|
134
|
+
"""Reads the RFT values from <RUNPATH>/<ECLBASE>.RFT
|
|
135
|
+
|
|
136
|
+
Also labels those values by which optionally zone constrained point
|
|
137
|
+
it belongs to.
|
|
138
|
+
|
|
139
|
+
The columns east, north, tvd is none when the value does not belong to
|
|
140
|
+
any point, otherwise it is the x,y,z values of that point. If the point
|
|
141
|
+
is constrained to be in a certain zone then the zone column is also populated.
|
|
142
|
+
|
|
143
|
+
Points which were constrained to be in a given zone, but were not contained
|
|
144
|
+
in that zone, is not labeled, and instead a warning is emitted.
|
|
145
|
+
"""
|
|
71
146
|
filename = substitute_runpath_name(self.input_files[0], iens, iter_)
|
|
72
147
|
if filename.upper().endswith(".DATA"):
|
|
73
148
|
# For backwards compatibility, it is
|
|
@@ -78,12 +153,12 @@ class RFTConfig(ResponseConfig):
|
|
|
78
153
|
if grid_filename.upper().endswith(".RFT"):
|
|
79
154
|
grid_filename = grid_filename[:-4]
|
|
80
155
|
grid_filename += ".EGRID"
|
|
81
|
-
fetched: dict[
|
|
82
|
-
|
|
83
|
-
)
|
|
156
|
+
fetched: dict[
|
|
157
|
+
tuple[WellName, datetime.date], dict[RFTProperty, npt.NDArray[np.float32]]
|
|
158
|
+
] = defaultdict(dict)
|
|
84
159
|
indices = {}
|
|
85
160
|
if self.locations:
|
|
86
|
-
indices = self._find_indices(grid_filename)
|
|
161
|
+
indices = self._filter_zones(self._find_indices(grid_filename), iens, iter_)
|
|
87
162
|
if None in indices:
|
|
88
163
|
raise InvalidResponseFile(
|
|
89
164
|
f"Did not find grid coordinate for location(s) {indices[None]}"
|
|
@@ -99,7 +174,10 @@ class RFTConfig(ResponseConfig):
|
|
|
99
174
|
"time": [],
|
|
100
175
|
"depth": [],
|
|
101
176
|
"values": [],
|
|
102
|
-
"
|
|
177
|
+
"east": [],
|
|
178
|
+
"north": [],
|
|
179
|
+
"tvd": [],
|
|
180
|
+
"zone": [],
|
|
103
181
|
}
|
|
104
182
|
)
|
|
105
183
|
|
|
@@ -146,7 +224,7 @@ class RFTConfig(ResponseConfig):
|
|
|
146
224
|
list(
|
|
147
225
|
indices.get(
|
|
148
226
|
(c[0] - 1, c[1] - 1, c[2] - 1),
|
|
149
|
-
[(
|
|
227
|
+
[_ZonedPoint()],
|
|
150
228
|
)
|
|
151
229
|
)
|
|
152
230
|
for c in entry.connections
|
|
@@ -165,7 +243,10 @@ class RFTConfig(ResponseConfig):
|
|
|
165
243
|
"time": [],
|
|
166
244
|
"depth": [],
|
|
167
245
|
"values": [],
|
|
168
|
-
"
|
|
246
|
+
"east": [],
|
|
247
|
+
"north": [],
|
|
248
|
+
"tvd": [],
|
|
249
|
+
"zone": [],
|
|
169
250
|
}
|
|
170
251
|
)
|
|
171
252
|
|
|
@@ -180,21 +261,37 @@ class RFTConfig(ResponseConfig):
|
|
|
180
261
|
"values": [vals],
|
|
181
262
|
"location": pl.Series(
|
|
182
263
|
[
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
264
|
+
[
|
|
265
|
+
[loc.point for loc in locs]
|
|
266
|
+
for locs in locations.get(
|
|
267
|
+
(well, time),
|
|
268
|
+
[[_ZonedPoint()]] * len(vals),
|
|
269
|
+
)
|
|
270
|
+
]
|
|
186
271
|
],
|
|
187
272
|
dtype=pl.Array(
|
|
188
273
|
pl.List(pl.Array(pl.Float32, 3)), len(vals)
|
|
189
274
|
),
|
|
190
275
|
),
|
|
276
|
+
"zone": pl.Series(
|
|
277
|
+
[
|
|
278
|
+
[
|
|
279
|
+
[loc.zone_name for loc in locs]
|
|
280
|
+
for locs in locations.get(
|
|
281
|
+
(well, time),
|
|
282
|
+
[[_ZonedPoint()]] * len(vals),
|
|
283
|
+
)
|
|
284
|
+
]
|
|
285
|
+
],
|
|
286
|
+
dtype=pl.Array(pl.List(pl.String), len(vals)),
|
|
287
|
+
),
|
|
191
288
|
}
|
|
192
289
|
)
|
|
193
|
-
.explode("depth", "values", "location")
|
|
194
|
-
.explode("location")
|
|
290
|
+
.explode("depth", "values", "location", "zone")
|
|
291
|
+
.explode("location", "zone")
|
|
195
292
|
for (well, time), inner_dict in fetched.items()
|
|
196
293
|
for prop, vals in inner_dict.items()
|
|
197
|
-
if prop != "DEPTH"
|
|
294
|
+
if prop != "DEPTH" and len(vals) > 0
|
|
198
295
|
]
|
|
199
296
|
)
|
|
200
297
|
except KeyError as err:
|
|
@@ -214,7 +311,7 @@ class RFTConfig(ResponseConfig):
|
|
|
214
311
|
|
|
215
312
|
@property
|
|
216
313
|
def primary_key(self) -> list[str]:
|
|
217
|
-
return ["east", "north", "tvd"]
|
|
314
|
+
return ["east", "north", "tvd", "zone"]
|
|
218
315
|
|
|
219
316
|
@classmethod
|
|
220
317
|
def from_config_dict(cls, config_dict: ConfigDict) -> RFTConfig | None:
|
|
@@ -236,8 +333,8 @@ class RFTConfig(ResponseConfig):
|
|
|
236
333
|
"step known to generate rft files"
|
|
237
334
|
)
|
|
238
335
|
|
|
239
|
-
declared_data: dict[
|
|
240
|
-
lambda: defaultdict(list)
|
|
336
|
+
declared_data: dict[WellName, dict[datetime.date, list[RFTProperty]]] = (
|
|
337
|
+
defaultdict(lambda: defaultdict(list))
|
|
241
338
|
)
|
|
242
339
|
for rft in rfts:
|
|
243
340
|
for expected in ["WELL", "DATE", "PROPERTIES"]:
|
|
@@ -267,6 +364,7 @@ class RFTConfig(ResponseConfig):
|
|
|
267
364
|
input_files=[eclbase.replace("%d", "<IENS>")],
|
|
268
365
|
keys=keys,
|
|
269
366
|
data_to_read=data_to_read,
|
|
367
|
+
zonemap=config_dict.get(ConfigKeys.ZONEMAP, ("", {}))[1],
|
|
270
368
|
)
|
|
271
369
|
|
|
272
370
|
return None
|
ert/config/summary_config.py
CHANGED
|
@@ -11,7 +11,7 @@ from ert.substitutions import substitute_runpath_name
|
|
|
11
11
|
from ._read_summary import read_summary
|
|
12
12
|
from .parsing import ConfigDict, ConfigKeys
|
|
13
13
|
from .parsing.config_errors import ConfigValidationError, ConfigWarning
|
|
14
|
-
from .response_config import InvalidResponseFile, ResponseConfig
|
|
14
|
+
from .response_config import InvalidResponseFile, ResponseConfig
|
|
15
15
|
from .responses_index import responses_index
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
@@ -21,18 +21,6 @@ class SummaryConfig(ResponseConfig):
|
|
|
21
21
|
type: Literal["summary"] = "summary"
|
|
22
22
|
has_finalized_keys: bool = False
|
|
23
23
|
|
|
24
|
-
@property
|
|
25
|
-
def metadata(self) -> list[ResponseMetadata]:
|
|
26
|
-
return [
|
|
27
|
-
ResponseMetadata(
|
|
28
|
-
response_type=self.type,
|
|
29
|
-
response_key=response_key,
|
|
30
|
-
filter_on=None,
|
|
31
|
-
finalized=self.has_finalized_keys,
|
|
32
|
-
)
|
|
33
|
-
for response_key in self.keys
|
|
34
|
-
]
|
|
35
|
-
|
|
36
24
|
@property
|
|
37
25
|
def expected_input_files(self) -> list[str]:
|
|
38
26
|
base = self.input_files[0]
|
ert/config/surface_config.py
CHANGED
|
@@ -10,11 +10,6 @@ import xarray as xr
|
|
|
10
10
|
from pydantic import field_serializer
|
|
11
11
|
from surfio import IrapHeader, IrapSurface
|
|
12
12
|
|
|
13
|
-
from ert.field_utils import (
|
|
14
|
-
calc_rho_for_2d_grid_layer,
|
|
15
|
-
transform_local_ellipse_angle_to_local_coords,
|
|
16
|
-
transform_positions_to_local_field_coordinates,
|
|
17
|
-
)
|
|
18
13
|
from ert.substitutions import substitute_runpath_name
|
|
19
14
|
|
|
20
15
|
from ._str_to_bool import str_to_bool
|
|
@@ -239,55 +234,3 @@ class SurfaceConfig(ParameterConfig):
|
|
|
239
234
|
this flattening process"""
|
|
240
235
|
|
|
241
236
|
return create_flattened_cube_graph(px=self.ncol, py=self.nrow, pz=1)
|
|
242
|
-
|
|
243
|
-
def calc_rho_for_2d_grid_layer(
|
|
244
|
-
self,
|
|
245
|
-
obs_xpos: npt.NDArray[np.float64],
|
|
246
|
-
obs_ypos: npt.NDArray[np.float64],
|
|
247
|
-
obs_main_range: npt.NDArray[np.float64],
|
|
248
|
-
obs_perp_range: npt.NDArray[np.float64],
|
|
249
|
-
obs_anisotropy_angle: npt.NDArray[np.float64],
|
|
250
|
-
) -> npt.NDArray[np.float64]:
|
|
251
|
-
"""Function to calculate scaling values to be used in the RHO matrix
|
|
252
|
-
for distance-based localization.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
obs_xpos: x-coordinates in global coordinates of observations
|
|
256
|
-
obs_ypos: y-coordinates in global coordinates of observations
|
|
257
|
-
obs_main_range: Size of influence ellipse main principal direction.
|
|
258
|
-
obs_perp_range: Size of influence ellipse second principal direction.
|
|
259
|
-
obs_anisotropy_angle: Rotation angle anticlock wise of main principal
|
|
260
|
-
direction of influence ellipse relative to global coordinate
|
|
261
|
-
system's x-axis.
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
Scaling values (elements of the RHO matrix) as a numpy array
|
|
265
|
-
of shape=(nx,ny,nobservations)
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
# Transform observation positions to local surface coordinates
|
|
269
|
-
xpos, ypos = transform_positions_to_local_field_coordinates(
|
|
270
|
-
(self.xori, self.yori), self.rotation, obs_xpos, obs_ypos
|
|
271
|
-
)
|
|
272
|
-
# Transform ellipse orientation to local surface coordinates
|
|
273
|
-
rotation_angle_of_localization_ellipse = (
|
|
274
|
-
transform_local_ellipse_angle_to_local_coords(
|
|
275
|
-
self.rotation, obs_anisotropy_angle
|
|
276
|
-
)
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# Assume the coordinate system is not flipped.
|
|
280
|
-
# This means the right_handed_grid_indexing is False
|
|
281
|
-
assert self.yflip == 1
|
|
282
|
-
return calc_rho_for_2d_grid_layer(
|
|
283
|
-
self.ncol,
|
|
284
|
-
self.nrow,
|
|
285
|
-
self.xinc,
|
|
286
|
-
self.yinc,
|
|
287
|
-
xpos,
|
|
288
|
-
ypos,
|
|
289
|
-
obs_main_range,
|
|
290
|
-
obs_perp_range,
|
|
291
|
-
rotation_angle_of_localization_ellipse,
|
|
292
|
-
right_handed_grid_indexing=False,
|
|
293
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from collections.abc import Mapping
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
import numpy.typing as npt
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _calculate_signed_chi_squared_misfit(
|
|
9
|
-
obs_value: npt.NDArray[np.float64],
|
|
10
|
-
response_value: npt.NDArray[np.float64],
|
|
11
|
-
obs_std: npt.NDArray[np.float64],
|
|
12
|
-
) -> list[float]:
|
|
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()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def calculate_signed_chi_squared_misfits(
|
|
20
|
-
reponses_dict: Mapping[int, pd.DataFrame],
|
|
21
|
-
observation: pd.DataFrame,
|
|
22
|
-
summary_misfits: bool = False,
|
|
23
|
-
) -> pd.DataFrame:
|
|
24
|
-
"""
|
|
25
|
-
Compute misfits from reponses_dict (real_id, values in dataframe)
|
|
26
|
-
and observation
|
|
27
|
-
"""
|
|
28
|
-
misfits_dict = {}
|
|
29
|
-
for realization_index in reponses_dict:
|
|
30
|
-
misfits_dict[realization_index] = _calculate_signed_chi_squared_misfit(
|
|
31
|
-
observation["values"],
|
|
32
|
-
reponses_dict[realization_index]
|
|
33
|
-
.loc[:, observation.index]
|
|
34
|
-
.to_numpy()
|
|
35
|
-
.flatten(),
|
|
36
|
-
observation["errors"],
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
df = pd.DataFrame(data=misfits_dict, index=observation.index)
|
|
40
|
-
if summary_misfits:
|
|
41
|
-
df = pd.DataFrame([df.abs().sum(axis=0)], columns=df.columns, index=[0])
|
|
42
|
-
return df.T
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
2
|
|
|
3
|
-
from .compute.misfits import router as misfits_router
|
|
4
3
|
from .ensembles import router as ensembles_router
|
|
5
4
|
from .experiment_server import router as experiment_server_router
|
|
6
5
|
from .experiments import router as experiments_router
|
|
@@ -15,7 +14,6 @@ router.include_router(experiments_router)
|
|
|
15
14
|
router.include_router(ensembles_router)
|
|
16
15
|
router.include_router(observations_router)
|
|
17
16
|
router.include_router(updates_router)
|
|
18
|
-
router.include_router(misfits_router)
|
|
19
17
|
router.include_router(parameters_router)
|
|
20
18
|
router.include_router(responses_router)
|
|
21
19
|
router.include_router(experiment_server_router)
|
|
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Body, Depends, HTTPException
|
|
|
6
6
|
from ert.config import SurfaceConfig
|
|
7
7
|
from ert.dark_storage import json_schema as js
|
|
8
8
|
from ert.dark_storage.common import get_storage
|
|
9
|
-
from ert.shared.storage.extraction import create_priors
|
|
10
9
|
from ert.storage import Storage
|
|
11
10
|
|
|
12
11
|
router = APIRouter(tags=["experiment"])
|
|
@@ -26,7 +25,6 @@ def get_experiments(
|
|
|
26
25
|
id=experiment.id,
|
|
27
26
|
name=experiment.name,
|
|
28
27
|
ensemble_ids=[ens.id for ens in experiment.ensembles],
|
|
29
|
-
priors=create_priors(experiment),
|
|
30
28
|
userdata={},
|
|
31
29
|
parameters={
|
|
32
30
|
group: config.model_dump()
|
|
@@ -34,7 +32,7 @@ def get_experiments(
|
|
|
34
32
|
if not isinstance(config, SurfaceConfig)
|
|
35
33
|
},
|
|
36
34
|
responses={
|
|
37
|
-
response_type:
|
|
35
|
+
response_type: config.model_dump()
|
|
38
36
|
for response_type, config in experiment.response_configuration.items()
|
|
39
37
|
},
|
|
40
38
|
observations=experiment.response_key_to_observation_key,
|
|
@@ -62,14 +60,13 @@ def get_experiment_by_id(
|
|
|
62
60
|
name=experiment.name,
|
|
63
61
|
id=experiment.id,
|
|
64
62
|
ensemble_ids=[ens.id for ens in experiment.ensembles],
|
|
65
|
-
priors=create_priors(experiment),
|
|
66
63
|
userdata={},
|
|
67
64
|
parameters={
|
|
68
65
|
group: config.model_dump()
|
|
69
66
|
for group, config in experiment.parameter_configuration.items()
|
|
70
67
|
},
|
|
71
68
|
responses={
|
|
72
|
-
response_type:
|
|
69
|
+
response_type: config.model_dump()
|
|
73
70
|
for response_type, config in experiment.response_configuration.items()
|
|
74
71
|
},
|
|
75
72
|
observations=experiment.response_key_to_observation_key,
|