-
Notifications
You must be signed in to change notification settings - Fork 121
Add grid properties #11851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add grid properties #11851
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,19 @@ | |
| import networkx as nx | ||
| import numpy as np | ||
| import xarray as xr | ||
| import xtgeo # type: ignore | ||
| from pydantic import field_serializer | ||
|
|
||
| from ert.field_utils import FieldFileFormat, Shape, read_field, read_mask, save_field | ||
| from ert.field_utils import ( | ||
| ErtboxParameters, | ||
| FieldFileFormat, | ||
| Shape, | ||
| calculate_ertbox_parameters, | ||
| get_shape, | ||
| read_field, | ||
| read_mask, | ||
| save_field, | ||
| ) | ||
| from ert.substitutions import substitute_runpath_name | ||
| from ert.utils import log_duration | ||
|
|
||
|
|
@@ -83,9 +93,7 @@ def adjust_graph_for_masking( | |
|
|
||
| class Field(ParameterConfig): | ||
| type: Literal["field"] = "field" | ||
| nx: int | ||
| ny: int | ||
| nz: int | ||
| ertbox_params: ErtboxParameters | ||
| file_format: FieldFileFormat | ||
| output_transformation: str | None | ||
| input_transformation: str | None | ||
|
|
@@ -115,20 +123,14 @@ def metadata(self) -> list[ParameterMetadata]: | |
| key=self.name, | ||
| transformation=self.output_transformation, | ||
| dimensionality=3, | ||
| userdata={ | ||
| "data_origin": "FIELD", | ||
| "nx": self.nx, | ||
| "ny": self.ny, | ||
| "nz": self.nz, | ||
| }, | ||
| userdata={"data_origin": "FIELD", "ertbox_params": self.ertbox_params}, | ||
| ) | ||
| ] | ||
|
|
||
| @classmethod | ||
| def from_config_list( | ||
| cls, | ||
| grid_file_path: str, | ||
| dims: Shape, | ||
| config_list: list[str | dict[str, str]], | ||
| ) -> Self: | ||
| name = cast(str, config_list[0]) | ||
|
|
@@ -198,11 +200,32 @@ def from_config_list( | |
| assert file_format is not None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could these 2 asserts be removed now?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I still need both asserts. |
||
|
|
||
| assert init_files is not None | ||
|
|
||
| grid_extension = Path(grid_file_path).suffix.lower() | ||
|
|
||
| try: | ||
| if grid_extension == ".egrid": | ||
| grid = xtgeo.grid_from_file(grid_file_path) | ||
| ertbox_params = calculate_ertbox_parameters(grid) | ||
| else: | ||
| dims = get_shape(grid_file_path) | ||
|
|
||
| if dims is None: | ||
| raise ConfigValidationError.with_context( | ||
| f"Grid file {grid_file_path} did not contain dimensions", | ||
| grid_file_path, | ||
| ) | ||
|
|
||
| ertbox_params = ErtboxParameters(dims.nx, dims.ny, dims.nz) | ||
| except Exception as err: | ||
| raise ConfigValidationError.with_context( | ||
| f"Could not read grid file {grid_file_path}: {err}", | ||
| grid_file_path, | ||
| ) from err | ||
|
|
||
| return cls( | ||
| name=name, | ||
| nx=dims.nx, | ||
| ny=dims.ny, | ||
| nz=dims.nz, | ||
| ertbox_params=ertbox_params, | ||
| file_format=file_format, | ||
| output_transformation=output_transform, | ||
| input_transformation=init_transform, | ||
|
|
@@ -217,7 +240,7 @@ def from_config_list( | |
|
|
||
| def __len__(self) -> int: | ||
| if self.mask_file is None: | ||
| return self.nx * self.ny * self.nz | ||
| return self.ertbox_params.nx * self.ertbox_params.ny * self.ertbox_params.nz | ||
|
|
||
| # Uses int() to convert to standard python int for mypy | ||
| return int(np.size(self.mask) - np.count_nonzero(self.mask)) | ||
|
|
@@ -236,7 +259,11 @@ def read_from_runpath( | |
| run_path / file_name, | ||
| self.name, | ||
| self.mask, | ||
| Shape(self.nx, self.ny, self.nz), | ||
| Shape( | ||
| self.ertbox_params.nx, | ||
| self.ertbox_params.ny, | ||
| self.ertbox_params.nz, | ||
| ), | ||
| ), | ||
| self.input_transformation, | ||
| ), | ||
|
|
@@ -332,10 +359,22 @@ def mask(self) -> Any: | |
|
|
||
| def load_parameter_graph(self) -> nx.Graph: # type: ignore | ||
| parameter_graph = create_flattened_cube_graph( | ||
| px=self.nx, py=self.ny, pz=self.nz | ||
| px=self.ertbox_params.nx, py=self.ertbox_params.ny, pz=self.ertbox_params.nz | ||
| ) | ||
| return adjust_graph_for_masking(G=parameter_graph, mask=self.mask.flatten()) | ||
|
|
||
| @property | ||
| def nx(self) -> int: | ||
| return self.ertbox_params.nx | ||
|
|
||
| @property | ||
| def ny(self) -> int: | ||
| return self.ertbox_params.ny | ||
|
|
||
| @property | ||
| def nz(self) -> int: | ||
| return self.ertbox_params.nz | ||
|
|
||
|
|
||
| TRANSFORM_FUNCTIONS = { | ||
| "LN": np.log, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,21 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import math | ||
| import os | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias | ||
|
|
||
| import numpy as np | ||
| import resfo | ||
| from pydantic.dataclasses import dataclass | ||
|
|
||
| from .field_file_format import ROFF_FORMATS, FieldFileFormat | ||
| from .grdecl_io import export_grdecl, import_bgrdecl, import_grdecl | ||
| from .roff_io import export_roff, import_roff | ||
|
|
||
| if TYPE_CHECKING: | ||
| import numpy.typing as npt | ||
| import xtgeo # type: ignore | ||
|
|
||
| _PathLike: TypeAlias = str | os.PathLike[str] | ||
|
|
||
|
|
@@ -96,6 +99,116 @@ def get_shape( | |
| return shape | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ErtboxParameters: | ||
| nx: int | ||
| ny: int | ||
| nz: int | ||
| xlength: float | None = None | ||
| ylength: float | None = None | ||
| xinc: float | None = None | ||
| yinc: float | None = None | ||
| rotation_angle: float | None = None | ||
| origin: tuple[float, float] | None = None | ||
|
|
||
|
|
||
| def calculate_ertbox_parameters( | ||
| grid: xtgeo.Grid, left_handed: bool = False | ||
| ) -> ErtboxParameters: | ||
| """Calculate ERTBOX grid parameters from an XTGeo grid. | ||
|
|
||
| Extracts geometric parameters including dimensions, cell increments, | ||
| rotation angle, and origin coordinates needed for ERTBOX. | ||
|
|
||
| Args: | ||
| grid: XTGeo Grid3D object | ||
| left_handed: If True, use left-handed coordinate system (default: False) | ||
|
|
||
| Returns: | ||
| ErtboxParameters with grid dimensions, increments, rotation, and origin | ||
| """ | ||
|
|
||
| (nx, ny, nz) = grid.dimensions | ||
|
|
||
| corner_indices = [] | ||
|
|
||
| if left_handed: | ||
| origin_cell = (1, 1, 1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a very naive question :) The cells start with (1,1,1) and not (0,0,0) ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's best if @oddvarlia answers this I think. |
||
| x_direction_cell = (nx, 1, 1) | ||
| y_direction_cell = (1, ny, 1) | ||
| else: | ||
| origin_cell = (1, ny, 1) | ||
| x_direction_cell = (nx, ny, 1) | ||
| y_direction_cell = (1, 1, 1) | ||
|
|
||
| corner_indices = [origin_cell, x_direction_cell, y_direction_cell] | ||
|
|
||
| # List with 3 elements, where each element contains the coordinates | ||
| # for all 8 corners of a single grid cell. | ||
| coord_cell = [] | ||
|
|
||
| for corner_index in corner_indices: | ||
| # Get real-world (x,y,z) coordinates for all 8 corners of this grid cell | ||
| # Returns 24 values: [x0,y0,z0, x1,y1,z1, ..., x7,y7,z7] | ||
| coord = grid.get_xyz_cell_corners(ijk=corner_index, activeonly=False) | ||
| coord_cell.append(coord) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion with vectorize approach: coords = [np.array(grid.get_xyz_cell_corners(ijk=idx, activeonly=False)).reshape(8, 3)
for idx in corner_indices]
if left_handed:
origin = coords[0][0, :2]
xdir = coords[1][1, :2]
ydir = coords[2][2, :2]
else:
origin = coords[0][2, :2]
xdir = coords[1][3, :2]
ydir = coords[2][0, :2]
xlength = np.linalg.norm(xdir - origin)
ylength = np.linalg.norm(ydir - origin)
xinc = xlength / nx
yinc = ylength / ny |
||
| if left_handed: | ||
| # Origin: cell (1,1,1), corner 0 | ||
| x0 = coord_cell[0][0] | ||
| y0 = coord_cell[0][1] | ||
|
|
||
| # X-direction: cell (nx,1,1), corner 1 | ||
| x1 = coord_cell[1][3] | ||
| y1 = coord_cell[1][4] | ||
|
|
||
| # Y-direction: cell (1,ny,1), corner 2 | ||
| x2 = coord_cell[2][6] | ||
| y2 = coord_cell[2][7] | ||
| else: | ||
| # Origin: cell (1,ny,1), corner 2 | ||
| x0 = coord_cell[0][6] | ||
| y0 = coord_cell[0][7] | ||
|
|
||
| # X-direction: cell (nx,ny,1), corner 3 | ||
| x1 = coord_cell[1][9] | ||
| y1 = coord_cell[1][10] | ||
|
|
||
| # Y-direction: cell (1,1,1), corner 0 | ||
| x2 = coord_cell[2][0] | ||
| y2 = coord_cell[2][1] | ||
|
|
||
| deltax1 = x1 - x0 | ||
| deltay1 = y1 - y0 | ||
|
|
||
| deltax2 = x2 - x0 | ||
| deltay2 = y2 - y0 | ||
|
|
||
| xlength = math.sqrt(deltax1**2 + deltay1**2) | ||
| ylength = math.sqrt(deltax2**2 + deltay2**2) | ||
| xinc = xlength / nx | ||
| yinc = ylength / ny | ||
|
|
||
| if math.fabs(deltax1) < 0.00001: | ||
| angle = 90.0 if deltay1 > 0 else -90.0 | ||
| elif deltax1 > 0: | ||
| angle = math.atan(deltay1 / deltax1) * 180.0 / math.pi | ||
| elif deltax1 < 0: | ||
| angle = (math.atan(deltay1 / deltax1) + math.pi) * 180.0 / math.pi | ||
|
|
||
| return ErtboxParameters( | ||
| nx=nx, | ||
| ny=ny, | ||
| nz=nz, | ||
| xlength=xlength, | ||
| ylength=ylength, | ||
| xinc=xinc, | ||
| yinc=yinc, | ||
| rotation_angle=angle, | ||
| origin=(x0, y0), | ||
| ) | ||
|
|
||
|
|
||
| def read_field( | ||
| field_path: _PathLike, | ||
| field_name: str, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could maybe all this grid relates parsing and loading be in the field config as classmethods?