Skip to content

Commit

Permalink
ENH: Add simplified export for depth prediction surfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
tnatt committed Mar 6, 2025
1 parent f073d3c commit 304a40e
Show file tree
Hide file tree
Showing 11 changed files with 412 additions and 5 deletions.
2 changes: 1 addition & 1 deletion docs/ext/pydantic_autosummary/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from enum import Enum
from typing import Any, Final, get_args, get_origin

_DATAIO_METADATA_PACKAGE: Final = "fmu.dataio._model"
_DATAIO_METADATA_PACKAGE: Final = "fmu.dataio._models"


def _is_dataio(annotation: Any) -> bool:
Expand Down
45 changes: 45 additions & 0 deletions docs/src/standard_results/structure_depth_surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Structure depth surfaces

This exports the modelled structural depth surfaces from within RMS.
These surfaces typically represent the final surface set generated during a structural
modelling workflow (after well conditioning), and frequently serve as the framework for
constructing the grid.

Note, it is only possible to export **one single set** of depth surface predictions per
model workflow. They represent the structural prediction of the model in depth.

:::{table} Current
:widths: auto
:align: left

| Field | Value |
| --- | --- |
| Version | NA |
| Output | `share/results/maps/structure_depth_surfaces/surfacename.gri` |
:::

## Requirements

- RMS
- depth surfaces stored in a horizon folder within RMS

The surfaces must be located within a horizon folder in RMS and be in domain `depth`.
This export function will automatically export all non-empty horizons from the provided folder.


## Usage

```{eval-rst}
.. autofunction:: fmu.dataio.export.rms.structure_depth_surfaces.export_structure_depth_surfaces
```

## Result

The surfaces from the horizon folder will be exported as 'irap_binary'
files to `share/results/maps/structure_depth_surfaces/surfacename.gri`.


## Standard result schema

This standard result is not presented in a tabular format; therefore, no validation
schema exists.
29 changes: 29 additions & 0 deletions schemas/0.9.0/fmu_results.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@
"oneOf": [
{
"$ref": "#/$defs/InplaceVolumesStandardResult"
},
{
"$ref": "#/$defs/StructureDepthSurfaceStandardResult"
}
],
"title": "AnyStandardResult"
Expand Down Expand Up @@ -7921,6 +7924,32 @@
"title": "StratigraphicColumn",
"type": "object"
},
"StructureDepthSurfaceStandardResult": {
"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.",
"properties": {
"file_schema": {
"anyOf": [
{
"$ref": "#/$defs/FileSchema"
},
{
"type": "null"
}
],
"default": null
},
"name": {
"const": "structure_depth_surface",
"title": "Name",
"type": "string"
}
},
"required": [
"name"
],
"title": "StructureDepthSurfaceStandardResult",
"type": "object"
},
"SubcropData": {
"description": "The ``data`` block contains information about the data contained in this object.\nThis class contains metadata for subcrops.",
"properties": {
Expand Down
1 change: 1 addition & 0 deletions src/fmu/dataio/_models/fmu_results/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class StandardResultName(str, Enum):
"""The standard result name of a given data object."""

inplace_volumes = "inplace_volumes"
structure_depth_surface = "structure_depth_surface"


class Classification(str, Enum):
Expand Down
16 changes: 15 additions & 1 deletion src/fmu/dataio/_models/fmu_results/standard_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ class InplaceVolumesStandardResult(StandardResult):
"""The schema identifying the format of the 'inplace_volumes' standard result."""


class StructureDepthSurfaceStandardResult(StandardResult):
"""
The ``standard_result`` field contains information about which standard results this
data object represent.
This class contains metadata for the 'structure_depth_surface' standard result.
"""

name: Literal[enums.StandardResultName.structure_depth_surface]
"""The identifying product name for the 'structure_depth_surface' product."""


class AnyStandardResult(RootModel):
"""
The ``standard result`` field contains information about which standard result this
Expand All @@ -70,6 +81,9 @@ class AnyStandardResult(RootModel):
"""

root: Annotated[
Union[InplaceVolumesStandardResult,],
Union[
InplaceVolumesStandardResult,
StructureDepthSurfaceStandardResult,
],
Field(discriminator="name"),
]
7 changes: 6 additions & 1 deletion src/fmu/dataio/export/rms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from .inplace_volumes import export_inplace_volumes, export_rms_volumetrics
from .structure_depth_surfaces import export_structure_depth_surfaces

__all__ = ["export_inplace_volumes", "export_rms_volumetrics"]
__all__ = [
"export_structure_depth_surfaces",
"export_inplace_volumes",
"export_rms_volumetrics",
]
28 changes: 28 additions & 0 deletions src/fmu/dataio/export/rms/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final

import xtgeo
from packaging.version import parse as versionparse

from fmu.dataio._logging import null_logger
Expand Down Expand Up @@ -65,3 +66,30 @@ def load_global_config() -> dict[str, Any]:
f"location: {CONFIG_PATH}."
)
return load_config_from_path(CONFIG_PATH)


def horizon_folder_exist(project: Any, horizon_folder: str) -> bool:
"""Check if a horizon folder exist inside the project"""
return horizon_folder in project.horizons.representations


def get_horizons_in_folder(
project: Any, horizon_folder: str
) -> list[xtgeo.RegularSurface]:
"""Get all non-empty horizons from a horizon folder stratigraphically ordered."""

logger.debug("Reading horizons from folder %s", horizon_folder)

if not horizon_folder_exist(project, horizon_folder):
raise ValueError(
f"The provided horizon folder name {horizon_folder} "
"does not exist inside RMS."
)

surfaces = []
for horizon in project.horizons:
if not horizon[horizon_folder].is_empty():
surfaces.append(
xtgeo.surface_from_roxar(project, horizon.name, horizon_folder)
)
return surfaces
114 changes: 114 additions & 0 deletions src/fmu/dataio/export/rms/structure_depth_surfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any, Final

import fmu.dataio as dio
from fmu.dataio._logging import null_logger
from fmu.dataio._models.fmu_results import standard_result
from fmu.dataio._models.fmu_results.enums import Classification, StandardResultName
from fmu.dataio.export._decorators import experimental
from fmu.dataio.export._export_result import ExportResult, ExportResultItem
from fmu.dataio.export.rms._utils import (
get_horizons_in_folder,
get_rms_project_units,
load_global_config,
)

if TYPE_CHECKING:
import xtgeo

_logger: Final = null_logger(__name__)


class _ExportStructureDepthSurfaces:
def __init__(
self,
project: Any,
horizon_folder: str,
) -> None:
_logger.debug("Process data, establish state prior to export.")
self._config = load_global_config()
self._surfaces = get_horizons_in_folder(project, horizon_folder)
self._unit = "m" if get_rms_project_units(project) == "metric" else "ft"

_logger.debug("Process data... DONE")

@property
def _standard_result(self) -> standard_result.StructureDepthSurfaceStandardResult:
"""Product type for the exported data."""
return standard_result.StructureDepthSurfaceStandardResult(
name=StandardResultName.structure_depth_surface
)

@property
def _classification(self) -> Classification:
"""Get default classification."""
return Classification.internal

def _export_surface(self, surf: xtgeo.RegularSurface) -> ExportResultItem:
edata = dio.ExportData(
config=self._config,
content="depth",
unit=self._unit,
vertical_domain="depth",
domain_reference="msl",
subfolder="structure_depth_surfaces",
is_prediction=True,
name=surf.name,
classification=self._classification,
rep_include=True,
)

absolute_export_path = edata._export_with_standard_result(
surf, standard_result=self._standard_result
)
_logger.debug("Surface exported to: %s", absolute_export_path)

return ExportResultItem(
absolute_path=Path(absolute_export_path),
)

def _export_surfaces(self) -> ExportResult:
"""Do the actual surface export using dataio setup."""
return ExportResult(
items=[self._export_surface(surf) for surf in self._surfaces]
)

def _validate_surfaces(self) -> None:
"""Surface validations."""
# TODO: Add check that the surfaces are consistent, i.e. a stratigraphic
# deeper surface should never have shallower values than the one above
# also check that the surfaces have a stratigraphy entry.

def export(self) -> ExportResult:
"""Export the depth as a standard_result."""
return self._export_surfaces()


@experimental
def export_structure_depth_surfaces(
project: Any,
horizon_folder: str,
) -> ExportResult:
"""Simplified interface when exporting modelled depth surfaces from RMS.
Args:
project: The 'magic' project variable in RMS.
horizon_folder: Name of horizon folder in RMS.
Note:
This function is experimental and may change in future versions.
Examples:
Example usage in an RMS script::
from fmu.dataio.export.rms import export_structure_depth_surfaces
export_results = export_structure_depth_surfaces(project, "DS_extracted")
for result in export_results.items:
print(f"Output surfaces to {result.absolute_path}")
"""

return _ExportStructureDepthSurfaces(project, horizon_folder).export()
21 changes: 19 additions & 2 deletions tests/test_export_rms/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def mock_rmsapi_jobs():
yield mock_rmsapi_jobs


@pytest.fixture
@pytest.fixture(autouse=True)
def mocked_rmsapi_modules(mock_rmsapi, mock_rmsapi_jobs):
with patch.dict(
sys.modules,
Expand All @@ -219,9 +219,26 @@ def mocked_rmsapi_modules(mock_rmsapi, mock_rmsapi_jobs):
yield mocked_modules


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

yield mock_project


@pytest.fixture
def xtgeo_surfaces(regsurf):
regsurf_top = regsurf.copy()
regsurf_top.name = "TopVolantis"

regsurf_mid = regsurf.copy()
regsurf_mid.name = "TopTherys"
regsurf_mid.values += 100

regsurf_base = regsurf.copy()
regsurf_base.name = "TopVolon"
regsurf_base.values += 200

yield [regsurf_top, regsurf_mid, regsurf_base]
Loading

0 comments on commit 304a40e

Please sign in to comment.