Skip to content

Commit 5ee130c

Browse files
authored
Calculate and store ERTBOX parameters
ERTBOX is a rectangular grid created in RMS that envelopes a field. From it, we calculate all parameters needed to do distance based localization. Since more information is written to storage, we do a storage migration and test that it works.
1 parent c2a7edc commit 5ee130c

24 files changed

Lines changed: 479 additions & 93 deletions

File tree

src/ert/config/ensemble_config.py

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import logging
44
from collections import Counter
5+
from pathlib import Path
56
from typing import Self
67

78
from pydantic import BaseModel, Field, model_validator
89

9-
from ert.field_utils import get_shape
10-
1110
from .ext_param_config import ExtParamConfig
1211
from .field import Field as FieldConfig
1312
from .gen_data_config import GenDataConfig
@@ -92,18 +91,13 @@ def from_dict(cls, config_dict: ConfigDict) -> EnsembleConfig:
9291
gen_kw_list = config_dict.get(ConfigKeys.GEN_KW, [])
9392
surface_list = config_dict.get(ConfigKeys.SURFACE, [])
9493
field_list = config_dict.get(ConfigKeys.FIELD, [])
95-
global_dims = None
9694

97-
# When users specify GRID as a separate line in the config,
98-
# and not as an option to the FIELD keyword.
9995
if global_grid_file_path is not None:
100-
try:
101-
global_dims = get_shape(global_grid_file_path)
102-
except Exception as err:
103-
raise ConfigValidationError.with_context(
104-
f"Could not read grid file {global_grid_file_path}: {err}",
105-
global_grid_file_path,
106-
) from err
96+
global_grid_file_path = Path(global_grid_file_path)
97+
98+
grid_extension = global_grid_file_path.suffix.lower()
99+
if grid_extension not in {".egrid", ".grid"}:
100+
raise ConfigValidationError("Only EGRID and GRID formats are supported")
107101

108102
def make_field(field_list: list[str | dict[str, str]]) -> FieldConfig:
109103
# An example of `field_list` when the keyword `GRID` is set:
@@ -124,26 +118,10 @@ def make_field(field_list: list[str | dict[str, str]]) -> FieldConfig:
124118

125119
# Use field-specific grid if provided,
126120
# otherwise fall back to global grid.
127-
if grid_file_path is not None:
128-
try:
129-
dims = get_shape(grid_file_path)
130-
except Exception as err:
131-
raise ConfigValidationError.with_context(
132-
f"Could not read grid file {grid_file_path}: {err}",
133-
grid_file_path,
134-
) from err
135-
else:
136-
grid_file_path = global_grid_file_path
137-
dims = global_dims
138-
139-
if dims is None:
140-
raise ConfigValidationError.with_context(
141-
f"Grid file {grid_file_path} did not contain dimensions",
142-
grid_file_path,
143-
)
144-
assert grid_file_path is not None
121+
if grid_file_path is None:
122+
grid_file_path = str(global_grid_file_path)
145123

146-
return FieldConfig.from_config_list(grid_file_path, dims, field_list)
124+
return FieldConfig.from_config_list(grid_file_path, field_list)
147125

148126
gen_kw_cfgs = [
149127
cfg for g in gen_kw_list for cfg in GenKwConfig.from_config_list(g)

src/ert/config/field.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@
1111
import networkx as nx
1212
import numpy as np
1313
import xarray as xr
14+
import xtgeo # type: ignore
1415
from pydantic import field_serializer
1516

16-
from ert.field_utils import FieldFileFormat, Shape, read_field, read_mask, save_field
17+
from ert.field_utils import (
18+
ErtboxParameters,
19+
FieldFileFormat,
20+
Shape,
21+
calculate_ertbox_parameters,
22+
get_shape,
23+
read_field,
24+
read_mask,
25+
save_field,
26+
)
1727
from ert.substitutions import substitute_runpath_name
1828
from ert.utils import log_duration
1929

@@ -83,9 +93,7 @@ def adjust_graph_for_masking(
8393

8494
class Field(ParameterConfig):
8595
type: Literal["field"] = "field"
86-
nx: int
87-
ny: int
88-
nz: int
96+
ertbox_params: ErtboxParameters
8997
file_format: FieldFileFormat
9098
output_transformation: str | None
9199
input_transformation: str | None
@@ -115,20 +123,14 @@ def metadata(self) -> list[ParameterMetadata]:
115123
key=self.name,
116124
transformation=self.output_transformation,
117125
dimensionality=3,
118-
userdata={
119-
"data_origin": "FIELD",
120-
"nx": self.nx,
121-
"ny": self.ny,
122-
"nz": self.nz,
123-
},
126+
userdata={"data_origin": "FIELD", "ertbox_params": self.ertbox_params},
124127
)
125128
]
126129

127130
@classmethod
128131
def from_config_list(
129132
cls,
130133
grid_file_path: str,
131-
dims: Shape,
132134
config_list: list[str | dict[str, str]],
133135
) -> Self:
134136
name = cast(str, config_list[0])
@@ -198,11 +200,32 @@ def from_config_list(
198200
assert file_format is not None
199201

200202
assert init_files is not None
203+
204+
grid_extension = Path(grid_file_path).suffix.lower()
205+
206+
try:
207+
if grid_extension == ".egrid":
208+
grid = xtgeo.grid_from_file(grid_file_path)
209+
ertbox_params = calculate_ertbox_parameters(grid)
210+
else:
211+
dims = get_shape(grid_file_path)
212+
213+
if dims is None:
214+
raise ConfigValidationError.with_context(
215+
f"Grid file {grid_file_path} did not contain dimensions",
216+
grid_file_path,
217+
)
218+
219+
ertbox_params = ErtboxParameters(dims.nx, dims.ny, dims.nz)
220+
except Exception as err:
221+
raise ConfigValidationError.with_context(
222+
f"Could not read grid file {grid_file_path}: {err}",
223+
grid_file_path,
224+
) from err
225+
201226
return cls(
202227
name=name,
203-
nx=dims.nx,
204-
ny=dims.ny,
205-
nz=dims.nz,
228+
ertbox_params=ertbox_params,
206229
file_format=file_format,
207230
output_transformation=output_transform,
208231
input_transformation=init_transform,
@@ -217,7 +240,7 @@ def from_config_list(
217240

218241
def __len__(self) -> int:
219242
if self.mask_file is None:
220-
return self.nx * self.ny * self.nz
243+
return self.ertbox_params.nx * self.ertbox_params.ny * self.ertbox_params.nz
221244

222245
# Uses int() to convert to standard python int for mypy
223246
return int(np.size(self.mask) - np.count_nonzero(self.mask))
@@ -236,7 +259,11 @@ def read_from_runpath(
236259
run_path / file_name,
237260
self.name,
238261
self.mask,
239-
Shape(self.nx, self.ny, self.nz),
262+
Shape(
263+
self.ertbox_params.nx,
264+
self.ertbox_params.ny,
265+
self.ertbox_params.nz,
266+
),
240267
),
241268
self.input_transformation,
242269
),
@@ -332,10 +359,22 @@ def mask(self) -> Any:
332359

333360
def load_parameter_graph(self) -> nx.Graph: # type: ignore
334361
parameter_graph = create_flattened_cube_graph(
335-
px=self.nx, py=self.ny, pz=self.nz
362+
px=self.ertbox_params.nx, py=self.ertbox_params.ny, pz=self.ertbox_params.nz
336363
)
337364
return adjust_graph_for_masking(G=parameter_graph, mask=self.mask.flatten())
338365

366+
@property
367+
def nx(self) -> int:
368+
return self.ertbox_params.nx
369+
370+
@property
371+
def ny(self) -> int:
372+
return self.ertbox_params.ny
373+
374+
@property
375+
def nz(self) -> int:
376+
return self.ertbox_params.nz
377+
339378

340379
TRANSFORM_FUNCTIONS = {
341380
"LN": np.log,

src/ert/field_utils/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
from __future__ import annotations
22

33
from .field_file_format import FieldFileFormat
4-
from .field_utils import Shape, get_shape, read_field, read_mask, save_field
4+
from .field_utils import (
5+
ErtboxParameters,
6+
Shape,
7+
calculate_ertbox_parameters,
8+
get_shape,
9+
read_field,
10+
read_mask,
11+
save_field,
12+
)
513

614
__all__ = [
15+
"ErtboxParameters",
716
"FieldFileFormat",
817
"Shape",
18+
"calculate_ertbox_parameters",
919
"get_shape",
1020
"read_field",
1121
"read_mask",

src/ert/field_utils/field_utils.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
from __future__ import annotations
22

3+
import math
34
import os
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias
67

78
import numpy as np
89
import resfo
10+
from pydantic.dataclasses import dataclass
911

1012
from .field_file_format import ROFF_FORMATS, FieldFileFormat
1113
from .grdecl_io import export_grdecl, import_bgrdecl, import_grdecl
1214
from .roff_io import export_roff, import_roff
1315

1416
if TYPE_CHECKING:
1517
import numpy.typing as npt
18+
import xtgeo # type: ignore
1619

1720
_PathLike: TypeAlias = str | os.PathLike[str]
1821

@@ -96,6 +99,116 @@ def get_shape(
9699
return shape
97100

98101

102+
@dataclass(frozen=True)
103+
class ErtboxParameters:
104+
nx: int
105+
ny: int
106+
nz: int
107+
xlength: float | None = None
108+
ylength: float | None = None
109+
xinc: float | None = None
110+
yinc: float | None = None
111+
rotation_angle: float | None = None
112+
origin: tuple[float, float] | None = None
113+
114+
115+
def calculate_ertbox_parameters(
116+
grid: xtgeo.Grid, left_handed: bool = False
117+
) -> ErtboxParameters:
118+
"""Calculate ERTBOX grid parameters from an XTGeo grid.
119+
120+
Extracts geometric parameters including dimensions, cell increments,
121+
rotation angle, and origin coordinates needed for ERTBOX.
122+
123+
Args:
124+
grid: XTGeo Grid3D object
125+
left_handed: If True, use left-handed coordinate system (default: False)
126+
127+
Returns:
128+
ErtboxParameters with grid dimensions, increments, rotation, and origin
129+
"""
130+
131+
(nx, ny, nz) = grid.dimensions
132+
133+
corner_indices = []
134+
135+
if left_handed:
136+
origin_cell = (1, 1, 1)
137+
x_direction_cell = (nx, 1, 1)
138+
y_direction_cell = (1, ny, 1)
139+
else:
140+
origin_cell = (1, ny, 1)
141+
x_direction_cell = (nx, ny, 1)
142+
y_direction_cell = (1, 1, 1)
143+
144+
corner_indices = [origin_cell, x_direction_cell, y_direction_cell]
145+
146+
# List with 3 elements, where each element contains the coordinates
147+
# for all 8 corners of a single grid cell.
148+
coord_cell = []
149+
150+
for corner_index in corner_indices:
151+
# Get real-world (x,y,z) coordinates for all 8 corners of this grid cell
152+
# Returns 24 values: [x0,y0,z0, x1,y1,z1, ..., x7,y7,z7]
153+
coord = grid.get_xyz_cell_corners(ijk=corner_index, activeonly=False)
154+
coord_cell.append(coord)
155+
156+
if left_handed:
157+
# Origin: cell (1,1,1), corner 0
158+
x0 = coord_cell[0][0]
159+
y0 = coord_cell[0][1]
160+
161+
# X-direction: cell (nx,1,1), corner 1
162+
x1 = coord_cell[1][3]
163+
y1 = coord_cell[1][4]
164+
165+
# Y-direction: cell (1,ny,1), corner 2
166+
x2 = coord_cell[2][6]
167+
y2 = coord_cell[2][7]
168+
else:
169+
# Origin: cell (1,ny,1), corner 2
170+
x0 = coord_cell[0][6]
171+
y0 = coord_cell[0][7]
172+
173+
# X-direction: cell (nx,ny,1), corner 3
174+
x1 = coord_cell[1][9]
175+
y1 = coord_cell[1][10]
176+
177+
# Y-direction: cell (1,1,1), corner 0
178+
x2 = coord_cell[2][0]
179+
y2 = coord_cell[2][1]
180+
181+
deltax1 = x1 - x0
182+
deltay1 = y1 - y0
183+
184+
deltax2 = x2 - x0
185+
deltay2 = y2 - y0
186+
187+
xlength = math.sqrt(deltax1**2 + deltay1**2)
188+
ylength = math.sqrt(deltax2**2 + deltay2**2)
189+
xinc = xlength / nx
190+
yinc = ylength / ny
191+
192+
if math.fabs(deltax1) < 0.00001:
193+
angle = 90.0 if deltay1 > 0 else -90.0
194+
elif deltax1 > 0:
195+
angle = math.atan(deltay1 / deltax1) * 180.0 / math.pi
196+
elif deltax1 < 0:
197+
angle = (math.atan(deltay1 / deltax1) + math.pi) * 180.0 / math.pi
198+
199+
return ErtboxParameters(
200+
nx=nx,
201+
ny=ny,
202+
nz=nz,
203+
xlength=xlength,
204+
ylength=ylength,
205+
xinc=xinc,
206+
yinc=yinc,
207+
rotation_angle=angle,
208+
origin=(x0, y0),
209+
)
210+
211+
99212
def read_field(
100213
field_path: _PathLike,
101214
field_name: str,

src/ert/gui/tools/plot/plot_window.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def updatePlot(self, layer: int | None = None) -> None:
247247
if "FIELD" in key_def.metadata["data_origin"]:
248248
plot_widget.showLayerWidget.emit(True)
249249

250-
layers = key_def.metadata["nz"]
250+
layers = key_def.metadata["ertbox_params"]["nz"]
251251
plot_widget.updateLayerWidget.emit(layers)
252252

253253
if layer is None:

src/ert/storage/local_storage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
logger = logging.getLogger(__name__)
3232

33-
_LOCAL_STORAGE_VERSION = 13
33+
_LOCAL_STORAGE_VERSION = 14
3434

3535

3636
class _Migrations(BaseModel):
@@ -493,6 +493,7 @@ def _migrate(self, version: int) -> None:
493493
to11,
494494
to12,
495495
to13,
496+
to14,
496497
)
497498

498499
try:
@@ -535,6 +536,7 @@ def _migrate(self, version: int) -> None:
535536
10: to11,
536537
11: to12,
537538
12: to13,
539+
13: to14,
538540
}
539541
for from_version in range(version, _LOCAL_STORAGE_VERSION):
540542
migrations[from_version].migrate(self.path)

0 commit comments

Comments
 (0)