Skip to content

Commit

Permalink
ENH: add one-liner function for volumetrics export in RMS
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrivenaes committed Jun 28, 2024
1 parent 63b6b3e commit 416ad20
Show file tree
Hide file tree
Showing 10 changed files with 632 additions and 7 deletions.
46 changes: 46 additions & 0 deletions docs/rms_oneliners.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
RMS targeted functions
======================

For lowerering the user threshold, some "one-liner" functions have been made for RMS. The purpose
is both to make it simpler for users to export certain items, and in addition secure a better
consistency. Hence the end user is not burdened to provide details, and only a script with quite
a few lines will be needed.

Currently only volumes are exposed, but this will be extended in the near future.

Exporting volumetrics from RMS
------------------------------

Volumetrics in RMS is always done in a so-called volume jobs. The intention with the simplification
is to use the RMS API behind the scene to retrieve all necessary data need for ``fmu.dataio``.

Example:

.. code-block:: python
from fmu.dataio.export.rms import RmsVolumetricsExport
...
# here 'Geogrid' is the grid model, and 'geogrid_volumes' is the name of the volume job
outfiles = RmsVolumetricsExport(project, "Geogrid", "geogrid_volumes").export()
print(f"Output volumes to {outfiles}")
Most ``dataio`` settings are here defaulted, but some keys can be altered optionally, e.g.:

.. code-block:: python
outfiles = RmsVolumetricsExport(
project,
"Geogrid",
"geogrid_volumes",
config_file="../whatever/global_variables.yml",
tagname="vol",
).export()
Details
-------

.. automodule:: fmu.dataio.export.rms.volumetrics
:members:
9 changes: 8 additions & 1 deletion docs/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# pylint: skip-file
import os
import sys
from pathlib import Path

from datetime import date

Expand Down Expand Up @@ -75,6 +74,14 @@
current_year = date.today().year
copyright = f"Equinor {current_year} (fmu-dataio release {release})"


# Sort members by input order in classes
autodoc_member_order = "bysource"
autodoc_default_flags = ["members", "show_inheritance"]

# Mocking ert and RMS modules
autodoc_mock_imports = ["ert", "rmsapi", "_rmsapi", "roxar", "_roxar"]

exclude_patterns = ["_build"]

pygments_style = "sphinx"
Expand Down
1 change: 1 addition & 0 deletions docs/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ post-processing services, new and improved cloud-only version of Webviz and much
overview
preparations
examples
rms_oneliners
apiref/modules
datamodel/index
21 changes: 15 additions & 6 deletions src/fmu/dataio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
"""Top-level package for fmu-dataio"""
# noqa

from fmu.dataio.dataio import AggregatedData # noqa # type: ignore
from fmu.dataio.dataio import ExportData # noqa # type: ignore
from fmu.dataio.dataio import InitializeCase # noqa # type: ignore
from fmu.dataio.dataio import read_metadata # noqa
from fmu.dataio.preprocessed import ExportPreprocessedData # noqa # type: ignore
from fmu.dataio.dataio import (
AggregatedData,
ExportData,
InitializeCase,
read_metadata,
)
from fmu.dataio.preprocessed import ExportPreprocessedData

try:
from .version import version

__version__ = version
except ImportError:
__version__ = "0.0.0"

__all__ = [
"AggregatedData",
"ExportData",
"InitializeCase",
"read_metadata",
"ExportPreprocessedData",
]
Empty file.
3 changes: 3 additions & 0 deletions src/fmu/dataio/export/rms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .volumetrics import RmsVolumetricsExport

__all__ = ["RmsVolumetricsExport"]
253 changes: 253 additions & 0 deletions src/fmu/dataio/export/rms/volumetrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Final
from warnings import warn

import pandas as pd
from packaging.version import parse as versionparse

import fmu.dataio as dio
from fmu.config.utilities import yaml_load
from fmu.dataio._logging import null_logger

try:
import roxar as rmsapi # noqa
import roxar.jobs as jobs # noqa
from _roxar import Project as RmsProject # noqa
except ImportError:
pass

try:
import rmsapi # noqa
import rmsapi.jobs as jobs # noqa
from _rmsapi import Project as RmsProject # noqa
except ImportError:
pass


_logger: Final = null_logger(__name__)

# rename columns to FMU standard
_RENAME_COLUMNS_FROM_RMS: Final = {
"Proj. real.": "REAL",
"Zone": "ZONE",
"Segment": "REGION",
"Boundary": "LICENSE",
"Facies": "FACIES",
"BulkOil": "BULK_OIL",
"NetOil": "NET_OIL",
"PoreOil": "PORV_OIL",
"HCPVOil": "HCPV_OIL",
"STOIIP": "STOIIP_OIL",
"AssociatedGas": "ASSOCIATEDGAS_OIL",
"BulkGas": "BULK_GAS",
"NetGas": "NET_GAS",
"PoreGas": "PORV_GAS",
"HCPVGas": "HCPV_GAS",
"GIIP": "GIIP_GAS",
"AssociatedLiquid": "ASSOCIATEDOIL_GAS",
"Bulk": "BULK_TOTAL",
"Net": "NET_TOTAL",
"Pore": "PORV_TOTAL",
}


@dataclass
class RmsVolumetricsExport:
"""Simplified interface when exporting volume tables (and assosiated data) from RMS.
As the export_volumetrics may have multiple output (storing both tables, maps and
3D grids), the output from this function is always a dictionary. The table is
mandatory output, while maps and 3D grid data are optional (not yet implemented).
Args:
project: The 'magic' project variable in RMS.
grid_name: Name of 3D grid model in RMS.
volume_job_name: Name of the volume job.
config_file: Optional. The file path to global configuration. The default
here is "../../fmuconfig/output/global_variables.yml. Hence it can be
omitted for those who follows the standard.
global_config: Optional. The global config as a dictionary. If present, it will
ovveride the ``config_file`` key. Hence using both config_file and
global_config is not recommended.
forcefolder: Optional. As default, volume tables will be exported to the agreed
file structure, and the folder name will be 'tables'. This can be
overriden here, but there will be warnings. For optional assosiated
volume maps and grids, the default folder names cannot be changed.
tagname: Optional. Defaulted to 'vol' for this function. Tagnames are part of
file names, and should not be applied as metadata.
classification: Optional. Use 'internal' or 'restricted' (default).
workflow: Optional. Information about the work flow; defaulted to
'rms volumetrics'.
include_zone_maps: Optional. If True, then all the zone maps that are toggled on
in the volume job are exported also (currently NOT implemented).
include_total_maps: Optional. If True, then all the total maps that are toggled
on in the volume job are exported also (currently NOT implemented).
include_3dgrid_propererties: Optional. If True, then all the 3D properties
(also called parameters) that are toggled on in the volume job are exported
also, together with the 3D grid geometry (currently NOT implemented).
"""

project: RmsProject
grid_name: str
volume_job_name: str

# optional and defaulted
config_file: str | Path = "../../fmuconfig/output/global_variables.yml"
global_config: dict = field(default_factory=dict)
forcefolder: str = "" # unsure if we shall allow this?
tagname: str = "vol"
classification: str = "restricted"
workflow: str = "rms volumetric run"
include_zone_maps: bool = False
include_total_maps: bool = False
include_3dgrid_properties: bool = False

# internal storage instance variables
_global_config: dict = field(default_factory=dict, init=False)
_volume_job: dict = field(default_factory=dict, init=False)
_volume_table_name: str = field(default="", init=False)
_dataframe: pd.DataFrame = field(default_factory=pd.DataFrame, init=False)
_units: str = field(default="metric", init=False)

@property
def dataframe(self) -> pd.DataFrame:
return self._dataframe

@staticmethod
def _check_rmsapi_version() -> None:
"""Check if we are working in a RMS API, and also check RMS versions?"""
_logger.debug("Check API version...")
if versionparse(rmsapi.__version__) < versionparse("1.7"):
raise RuntimeError(
"You need at least API version 1.7 (RMS 13.1) to use this function."
)
_logger.debug("Check API version... DONE")

def _set_global_config(self) -> None:
"""Set the global config data by reading the file."""
# TODO: This functionality should ideally be in fmu-config.
_logger.debug("Set global config...")

if self.global_config:
self._global_config = self.global_config
_logger.debug("Set global config (from input dictionary)... DONE!")
return

global_config_path = Path(self.config_file)

if global_config_path.is_file():
self._global_config = yaml_load(global_config_path)
else:
raise ValueError(f"Cannot find file for global config: {self.config_file}")
_logger.debug("Set global config (from file)... DONE!")

def _rms_volume_job_settings(self) -> None:
"""Get information out from the RMS job API."""
_logger.debug("Check RMS VOLJOB settings...")
self._volume_job = jobs.Job.get_job(
owner=["Grid models", self.grid_name, "Grid"],
type="Volumetrics",
name=self.volume_job_name,
).get_arguments()
_logger.debug("RMS VOLJOB settings... DONE")

def _read_volume_table_name_from_rms(self) -> None:
"""Read the volume table name from RMS."""
_logger.debug("Read volume table name from RMS...")
voltable = self._volume_job.get("Report")
if isinstance(voltable, list):
voltable = voltable[0]
self._volume_table_name = voltable.get("ReportTableName")

if not self._volume_table_name:
raise RuntimeError(
"You need to configure output to Report file: Report table "
"in the volumetric job. Provide a table name and rerun."
)

_logger.debug("The volume table name is %s", self._volume_table_name)
_logger.debug("Read volume table name from RMS... DONE")

def _voltable_as_dataframe(self) -> None:
"""Convert table to pandas dataframe"""
_logger.debug("Read values and convert to pandas dataframe...")
dict_values = (
self.project.volumetric_tables[self._volume_table_name]
.get_data_table()
.to_dict()
)
_logger.debug("Dict values are: %s", dict_values)
self._dataframe = dfr = pd.DataFrame.from_dict(dict_values)
dfr.rename(columns=_RENAME_COLUMNS_FROM_RMS, inplace=True)
dfr.drop("REAL", axis=1, inplace=True, errors="ignore")
_logger.debug("Read values and convert to pandas dataframe... DONE")

def _set_units(self) -> None:
"""See if the RMS project is defined in metric or feet."""

units = self.project.project_units
_logger.debug("Units are %s", units)
self._units = str(units)

def _read_includes_etc(self) -> None:
"""Handle other products (placeholder; in prep in the code)."""
if self.include_zone_maps:
raise NotImplementedError(
"Including zone maps is currently not implemented"
)
if self.include_total_maps:
raise NotImplementedError(
"Including total maps is currently not implemented"
)
if self.include_3dgrid_properties:
raise NotImplementedError(
"Including 3D parameters is currently not implemented"
)

def _warn_if_forcefolder(self) -> None:
if self.forcefolder:
warn(
"A 'forcefolder' is set. This is strongly discouraged and will be "
"removed in coming versions",
FutureWarning,
)

def _process_data(self) -> None:
"""Process all routines ^ except exports."""
_logger.debug("Process data...")
self._check_rmsapi_version()
self._set_global_config()
self._rms_volume_job_settings()
self._read_volume_table_name_from_rms()
self._voltable_as_dataframe()
self._set_units()
self._read_includes_etc()
self._warn_if_forcefolder()
_logger.debug("Process data... DONE")

def _export_volume_table(self) -> dict[str, str]:
"""Do the actual volume table export using dataio setup."""

edata = dio.ExportData(
config=self._global_config,
content="volumes",
unit="m3" if self._units == "metric" else "ft3",
vertical_domain={"depth": "msl"},
workflow=self.workflow,
forcefolder=self.forcefolder,
classification=self.classification,
tagname=self.tagname,
name=self.grid_name,
)

out = edata.export(self.dataframe)
_logger.debug("Volume result to: %s", out)
return {"volume_table": out}

def export(self) -> dict[str, str]: # public function
"""Export the volume table."""
self._process_data()
return self._export_volume_table()
Loading

0 comments on commit 416ad20

Please sign in to comment.