Skip to content

Commit 87ac7aa

Browse files
committed
ENH: Add simplified export for depth prediction surfaces
1 parent 289a3a3 commit 87ac7aa

File tree

10 files changed

+411
-4
lines changed

10 files changed

+411
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Structure depth surfaces
2+
3+
This exports the modelled structural depth surfaces from within RMS.
4+
These surfaces typically represent the final surface set generated during a structural
5+
modeling workflow (after well conditioning), and frequently serve as the framework for
6+
constructing the grid.
7+
8+
note::
9+
It is only possible to export **one single set** of depth surface predictions per model
10+
workflow. They represent the structural prediction of the model in depth.
11+
12+
:::{table} Current
13+
:widths: auto
14+
:align: left
15+
16+
| Field | Value |
17+
| --- | --- |
18+
| Version | NA |
19+
| Output | `share/results/maps/structure_depth_surfaces/surfacename.gri` |
20+
:::
21+
22+
## Requirements
23+
24+
- RMS
25+
- depth surfaces stored in a horizon folder within RMS
26+
27+
The surfaces must be located within a horizon folder in RMS and be in domain `depth`.
28+
This export function will automatically export all non-empty horizons from the provided folder.
29+
30+
31+
## Usage
32+
33+
```{eval-rst}
34+
.. autofunction:: fmu.dataio.export.rms.structure_depth_surfaces.export_structure_depth_surfaces
35+
```
36+
37+
## Result
38+
39+
The surfaces from within the horizon folder will be exported as 'irap_binary'
40+
files to `share/results/maps/structure_depth_surfaces/surfacename.gri`.
41+
42+
43+
## Standard result schema
44+
45+
This standard result is not presented in a tabular format; therefore, no validation
46+
schema exists.
47+
48+

schemas/0.9.0/fmu_results.json

+29
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@
219219
"oneOf": [
220220
{
221221
"$ref": "#/$defs/InplaceVolumesStandardResult"
222+
},
223+
{
224+
"$ref": "#/$defs/SructureDepthSurfaceStandardResult"
222225
}
223226
],
224227
"title": "AnyStandardResult"
@@ -7849,6 +7852,32 @@
78497852
"title": "Smda",
78507853
"type": "object"
78517854
},
7855+
"SructureDepthSurfaceStandardResult": {
7856+
"description": "The ``standard_result`` field contains information about which standard results this\ndata object represent.\nThis class contains metadata for the 'structure_depth_surface' standard result.",
7857+
"properties": {
7858+
"file_schema": {
7859+
"anyOf": [
7860+
{
7861+
"$ref": "#/$defs/FileSchema"
7862+
},
7863+
{
7864+
"type": "null"
7865+
}
7866+
],
7867+
"default": null
7868+
},
7869+
"name": {
7870+
"const": "structure_depth_surface",
7871+
"title": "Name",
7872+
"type": "string"
7873+
}
7874+
},
7875+
"required": [
7876+
"name"
7877+
],
7878+
"title": "SructureDepthSurfaceStandardResult",
7879+
"type": "object"
7880+
},
78527881
"Ssdl": {
78537882
"description": "The ``access.ssdl`` block contains information related to SSDL.\nNote that this is kept due to legacy.",
78547883
"properties": {

src/fmu/dataio/_models/fmu_results/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class StandardResultName(str, Enum):
88
"""The standard result name of a given data object."""
99

1010
inplace_volumes = "inplace_volumes"
11+
structure_depth_surface = "structure_depth_surface"
1112

1213

1314
class Classification(str, Enum):

src/fmu/dataio/_models/fmu_results/standard_result.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ class InplaceVolumesStandardResult(StandardResult):
5858
"""The schema identifying the format of the 'inplace_volumes' standard result."""
5959

6060

61+
class SructureDepthSurfaceStandardResult(StandardResult):
62+
"""
63+
The ``standard_result`` field contains information about which standard results this
64+
data object represent.
65+
This class contains metadata for the 'structure_depth_surface' standard result.
66+
"""
67+
68+
name: Literal[enums.StandardResultName.structure_depth_surface]
69+
"""The identifying product name for the 'structure_depth_surface' product."""
70+
71+
6172
class AnyStandardResult(RootModel):
6273
"""
6374
The ``standard result`` field contains information about which standard result this
@@ -70,6 +81,9 @@ class AnyStandardResult(RootModel):
7081
"""
7182

7283
root: Annotated[
73-
Union[InplaceVolumesStandardResult,],
84+
Union[
85+
InplaceVolumesStandardResult,
86+
SructureDepthSurfaceStandardResult,
87+
],
7488
Field(discriminator="name"),
7589
]

src/fmu/dataio/export/rms/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
from .inplace_volumes import export_inplace_volumes, export_rms_volumetrics
2+
from .structure_depth_surfaces import export_structure_depth_surfaces
23

3-
__all__ = ["export_inplace_volumes", "export_rms_volumetrics"]
4+
__all__ = [
5+
"export_structure_depth_surfaces",
6+
"export_inplace_volumes",
7+
"export_rms_volumetrics",
8+
]

src/fmu/dataio/export/rms/_utils.py

+25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44
from typing import TYPE_CHECKING, Any, Final
55

6+
import xtgeo
67
from packaging.version import parse as versionparse
78

89
from fmu.dataio._logging import null_logger
@@ -65,3 +66,27 @@ def load_global_config() -> dict[str, Any]:
6566
f"location: {CONFIG_PATH}."
6667
)
6768
return load_config_from_path(CONFIG_PATH)
69+
70+
71+
def horizon_folder_exist(project: Any, horizon_folder: str) -> bool:
72+
"""Check if a horizon folder exist inside the project"""
73+
return horizon_folder in project.horizons.representations
74+
75+
76+
def get_horizons_in_folder(
77+
project: Any, horizon_folder: str
78+
) -> list[xtgeo.RegularSurface]:
79+
"""Get all non-empty horizons from a horizon folder stratigraphically ordered."""
80+
81+
logger.debug("Reading horizons from folder %s", horizon_folder)
82+
83+
if not horizon_folder_exist(project, horizon_folder):
84+
raise ValueError(f"The {horizon_folder=} does not exist inside RMS")
85+
86+
surfaces = []
87+
for horizon in project.horizons:
88+
if not horizon[horizon_folder].is_empty():
89+
surfaces.append(
90+
xtgeo.surface_from_roxar(project, horizon.name, horizon_folder)
91+
)
92+
return surfaces
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING, Any, Final
5+
6+
import fmu.dataio as dio
7+
from fmu.dataio._logging import null_logger
8+
from fmu.dataio._models.fmu_results import standard_result
9+
from fmu.dataio._models.fmu_results.enums import Classification, StandardResultName
10+
from fmu.dataio.export._decorators import experimental
11+
from fmu.dataio.export._export_result import ExportResult, ExportResultItem
12+
from fmu.dataio.export.rms._utils import (
13+
get_horizons_in_folder,
14+
get_rms_project_units,
15+
load_global_config,
16+
)
17+
18+
if TYPE_CHECKING:
19+
import xtgeo
20+
21+
_logger: Final = null_logger(__name__)
22+
23+
24+
class _ExportStructureDepthSurfaces:
25+
def __init__(
26+
self,
27+
project: Any,
28+
horizon_folder: str,
29+
):
30+
_logger.debug("Process data, establish state prior to export.")
31+
self._config = load_global_config()
32+
self._surfaces = get_horizons_in_folder(project, horizon_folder)
33+
self._unit = "m" if get_rms_project_units(project) == "metric" else "ft"
34+
35+
_logger.debug("Process data... DONE")
36+
37+
@property
38+
def _standard_result(self) -> standard_result.SructureDepthSurfaceStandardResult:
39+
"""Product type for the exported data."""
40+
return standard_result.SructureDepthSurfaceStandardResult(
41+
name=StandardResultName.structure_depth_surface
42+
)
43+
44+
@property
45+
def _classification(self) -> Classification:
46+
"""Get default classification."""
47+
return Classification.internal
48+
49+
def _export_surface(self, surf: xtgeo.RegularSurface) -> ExportResultItem:
50+
edata = dio.ExportData(
51+
config=self._config,
52+
content="depth",
53+
unit=self._unit,
54+
vertical_domain="depth",
55+
domain_reference="msl",
56+
subfolder="structure_depth_surfaces",
57+
is_prediction=True,
58+
name=surf.name,
59+
classification=self._classification,
60+
rep_include=True,
61+
)
62+
63+
absolute_export_path = edata._export_with_standard_result(
64+
surf, standard_result=self._standard_result
65+
)
66+
_logger.debug("Surface exported to: %s", absolute_export_path)
67+
68+
return ExportResultItem(
69+
absolute_path=Path(absolute_export_path),
70+
)
71+
72+
def _export_surfaces(self) -> ExportResult:
73+
"""Do the actual surface export using dataio setup."""
74+
return ExportResult(
75+
items=[self._export_surface(surf) for surf in self._surfaces]
76+
)
77+
78+
def _validate_surfaces(self) -> None:
79+
"""Surface validations."""
80+
# TODO: Add check that the surfaces are consistent, i.e. a stratigraphic
81+
# deeper surface should never have shallower values than the one above
82+
# also check that the surfaces have a stratigraphy entry.
83+
84+
def export(self) -> ExportResult:
85+
"""Export the depth as a standard_result."""
86+
return self._export_surfaces()
87+
88+
89+
@experimental
90+
def export_structure_depth_surfaces(
91+
project: Any,
92+
horizon_folder: str,
93+
) -> ExportResult:
94+
"""Simplified interface when exporting modelled depth surfaces from RMS.
95+
96+
Args:
97+
project: The 'magic' project variable in RMS.
98+
horizon_folder: Name of horizon folder in RMS.
99+
Note:
100+
This function is experimental and may change in future versions.
101+
102+
Examples:
103+
Example usage in an RMS script::
104+
105+
from fmu.dataio.export.rms import export_structure_depth_surfaces
106+
107+
export_results = export_structure_depth_surfaces(project, "DS_extracted")
108+
109+
for result in export_results.items:
110+
print(f"Output surfaces to {result.absolute_path}")
111+
112+
"""
113+
114+
return _ExportStructureDepthSurfaces(project, horizon_folder).export()

tests/test_export_rms/conftest.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def mock_rmsapi_jobs():
207207
yield mock_rmsapi_jobs
208208

209209

210-
@pytest.fixture
210+
@pytest.fixture(autouse=True)
211211
def mocked_rmsapi_modules(mock_rmsapi, mock_rmsapi_jobs):
212212
with patch.dict(
213213
sys.modules,
@@ -219,9 +219,26 @@ def mocked_rmsapi_modules(mock_rmsapi, mock_rmsapi_jobs):
219219
yield mocked_modules
220220

221221

222-
@pytest.fixture(autouse=True)
222+
@pytest.fixture
223223
def mock_project_variable():
224224
# A mock_project variable for the RMS 'project' (potentially extend for later use)
225225
mock_project = MagicMock()
226+
mock_project.horizons.representations = ["DS_final"]
226227

227228
yield mock_project
229+
230+
231+
@pytest.fixture
232+
def xtgeo_surfaces(regsurf):
233+
regsurf_top = regsurf.copy()
234+
regsurf_top.name = "TopVolantis"
235+
236+
regsurf_mid = regsurf.copy()
237+
regsurf_mid.name = "TopTherys"
238+
regsurf_mid.values += 100
239+
240+
regsurf_base = regsurf.copy()
241+
regsurf_base.name = "TopVolon"
242+
regsurf_base.values += 200
243+
244+
yield [regsurf_top, regsurf_mid, regsurf_base]

0 commit comments

Comments
 (0)