Skip to content

Commit

Permalink
ENH: support for RoxAPI general2d_data and trends
Browse files Browse the repository at this point in the history
Support Roxar API support for "RMS General 2D data" for points,
polygons and regular surfaces when RoxAPI version >= 1.6 (RMS13+). For
surfaces, also Trends are supported.

Also,  from Roxar API version 1.6, setting and getting points
(and polygons)  attributes are supported. In earlier versions,
xtgeo use a file workaround which is much slower. Polygon
attributes are not implemented in this PR.
  • Loading branch information
jcrivenaes committed Jun 22, 2022
1 parent 0038365 commit 547d39d
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 146 deletions.
104 changes: 86 additions & 18 deletions src/xtgeo/surface/_regsurf_roxapi.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
# coding: utf-8
"""Roxar API functions for XTGeo RegularSurface."""
from xtgeo.common import XTGeoDialog
import os
import tempfile

from xtgeo import RoxUtils
from xtgeo.common import XTGeoDialog

xtg = XTGeoDialog()

logger = xtg.functionlogger(__name__)

VALID_STYPES = ["horizons", "zones", "clipboard", "general2d_data", "trends"]


def _check_stypes_names_category(roxutils, stype, name, category):
"""General check of some input values."""
stype = stype.lower()

if stype not in VALID_STYPES:
raise ValueError(
f"Given stype {stype} is not supported, legal stypes are: {VALID_STYPES}"
)

if not name:
raise ValueError("The name is missing or empty.")

if stype in ("horizons", "zones") and (name is None or not category):
raise ValueError(
"Need to spesify both name and category for horizons and zones"
)

if stype == "general2d_data" and not roxutils.version_required("1.6"):
raise NotImplementedError(
"API Support for general2d_data is missing in this RMS version"
f"(current API version is {roxutils.roxversion} - required is 1.6"
)


def import_horizon_roxapi(
project, name, category, stype, realisation
): # pragma: no cover
"""Import a Horizon surface via ROXAR API spec."""
rox = RoxUtils(project, readonly=True)
"""Import a Horizon surface via ROXAR API spec. to xtgeo."""
roxutils = RoxUtils(project, readonly=True)

proj = rox.project
_check_stypes_names_category(roxutils, stype, name, category)

proj = roxutils.project
args = _roxapi_import_surface(proj, name, category, stype, realisation)

rox.safe_close()
roxutils.safe_close()
return args


Expand Down Expand Up @@ -54,20 +84,32 @@ def _roxapi_import_surface(
except KeyError as kwe:
logger.error(kwe)

elif stype == "clipboard":
elif stype in ("clipboard", "general2d_data"):
styperef = getattr(proj, stype)
if category:
if "|" in category:
folders = category.split("|")
else:
folders = category.split("/")
rox = proj.clipboard.folders[folders]
rox = styperef.folders[folders]
else:
rox = proj.clipboard
rox = styperef

roxsurf = rox[name].get_grid(realisation)
args.update(_roxapi_horizon_to_xtgeo(roxsurf))

elif stype == "trends":

if name not in proj.trends.surfaces:
logger.info("Name %s is not present in trends", name)
raise ValueError(f"Name {name} is not within Trends")
rox = proj.trends.surfaces[name]

roxsurf = rox.get_grid(realisation)
args.update(_roxapi_horizon_to_xtgeo(roxsurf))

else:
raise ValueError("Invalid stype")
raise ValueError(f"Invalid stype given: {stype}") # should never reach here
return args


Expand All @@ -90,16 +132,18 @@ def export_horizon_roxapi(
self, project, name, category, stype, realisation
): # pragma: no cover
"""Export (store) a Horizon surface to RMS via ROXAR API spec."""
rox = RoxUtils(project, readonly=False)
roxutils = RoxUtils(project, readonly=False)

_check_stypes_names_category(roxutils, stype, name, category)

logger.info("Surface from xtgeo to roxapi...")
_roxapi_export_surface(self, rox.project, name, category, stype, realisation)
_roxapi_export_surface(self, roxutils.project, name, category, stype, realisation)

if rox._roxexternal:
rox.project.save()
if roxutils._roxexternal:
roxutils.project.save()

logger.info("Surface from xtgeo to roxapi... DONE")
rox.safe_close()
roxutils.safe_close()


def _roxapi_export_surface(
Expand Down Expand Up @@ -135,22 +179,46 @@ def _roxapi_export_surface(
except KeyError as kwe:
logger.error(kwe)

elif stype == "clipboard":
elif stype in ("clipboard", "general2d_data"):
folders = []
if category:
if "|" in category:
folders = category.split("|")
else:
folders = category.split("/")
styperef = getattr(proj, stype)
if folders:
proj.clipboard.folders.create(folders)
styperef.folders.create(folders)

roxroot = proj.clipboard.create_surface(name, folders)
roxroot = styperef.create_surface(name, folders)
roxg = _xtgeo_to_roxapi_grid(self)
roxg.set_values(self.values)
roxroot.set_grid(roxg)

elif stype == "trends":
if name not in proj.trends.surfaces:
logger.info("Name %s is not present in trends", name)
raise ValueError(
f"Name {name} is not within Trends (it must exist in advance!)"
)
# here a workound; trends.surfaces are read-only in Roxar API, but is seems
# that load() in RMS is an (undocumented?) workaround...
try:
import roxar # pylint: disable=import-outside-toplevel
except ImportError as err:
raise ImportError(
"roxar not available, this functionality is not available"
) from err

roxsurf = proj.trends.surfaces[name]
with tempfile.TemporaryDirectory() as tmpdir:
logger.info("Made a tmp folder: %s", tmpdir)
self.to_file(os.path.join(tmpdir, "gxx.gri"), fformat="irap_binary")

roxsurf.load(os.path.join(tmpdir, "gxx.gri"), roxar.FileFormat.ROXAR_BINARY)

else:
raise ValueError("Invalid stype")
raise ValueError(f"Invalid stype given: {stype}") # should never reach here


def _xtgeo_to_roxapi_grid(self): # pragma: no cover
Expand Down
67 changes: 37 additions & 30 deletions src/xtgeo/surface/regular_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ def surface_from_roxar(project, name, category, stype="horizons", realisation=0)
project (str or special): Name of project (as folder) if
outside RMS, og just use the magic project word if within RMS.
name (str): Name of surface/map
category (str): For horizons/zones or clipboard: for example 'DS_extracted'
stype (str): RMS folder type, 'horizons' (default), 'zones' or 'clipboard'
category (str): For horizons/zones or clipboard/general2d_data:
for example 'DS_extracted'. For clipboard/general2d_data this can
be empty or None, or use '/' for multiple folder levels (e.g. 'fld/subfld').
For 'trends', the category is not applied.
stype (str): RMS folder type, 'horizons' (default), 'zones', 'clipboard',
'general2d_data' or 'trends'
realisation (int): Realisation number, default is 0
Example::
Expand All @@ -121,6 +125,14 @@ def surface_from_roxar(project, name, category, stype="horizons", realisation=0)
import xtgeo
mysurf = xtgeo.surface_from_roxar(project, 'TopEtive', 'DepthSurface')
Note::
When dealing with surfaces to and from ``stype="trends"``, the surface must
exist in advance, i.e. the Roxar API do not allow creating new surfaces.
Actually trends are read only, but a workaround using ``load()`` in Roxar
API makes it possible to overwrite existing surface trends. In addition,
``realisation`` is not applied in trends.
"""

return RegularSurface._read_roxar(
Expand Down Expand Up @@ -1320,19 +1332,12 @@ def _read_roxar(
project (str or special): Name of project (as folder) if
outside RMS, og just use the magic project word if within RMS.
name (str): Name of surface/map
category (str): For horizons/zones or clipboard: for example 'DS_extracted'
category (str): For horizons/zones or clipboard/general2d_data: for
example 'DS_extracted'
stype (str): RMS folder type, 'horizons' (default), 'zones' or 'clipboard'
realisation (int): Realisation number, default is 0
"""
stype = stype.lower()
valid_stypes = ["horizons", "zones", "clipboard"]

if stype not in valid_stypes:
raise ValueError(
"Invalid stype, only {} stypes is supported.".format(valid_stypes)
)

kwargs = _regsurf_roxapi.import_horizon_roxapi(
project, name, category, stype, realisation
)
Expand Down Expand Up @@ -1371,8 +1376,10 @@ def from_roxar(
project (str or special): Name of project (as folder) if
outside RMS, og just use the magic project word if within RMS.
name (str): Name of surface/map
category (str): For horizons/zones or clipboard: for example 'DS_extracted'
stype (str): RMS folder type, 'horizons' (default), 'zones' or 'clipboard'
category (str): For horizons/zones or clipboard/general2d_data: for
example 'DS_extracted'
stype (str): RMS folder type, 'horizons' (default), 'zones', 'clipboard'
or 'general2d_data'
realisation (int): Realisation number, default is 0
Returns:
Expand All @@ -1390,13 +1397,6 @@ def from_roxar(
"""
valid_stypes = ["horizons", "zones", "clipboard"]

if stype.lower() not in valid_stypes:
raise ValueError(
"Invalid stype, only {} stypes is supported.".format(valid_stypes)
)

kwargs = _regsurf_roxapi.import_horizon_roxapi(
project, name, category, stype, realisation
)
Expand Down Expand Up @@ -1425,8 +1425,13 @@ def to_roxar(
project (str or special): Name of project (as folder) if
outside RMS, og just use the magic project word if within RMS.
name (str): Name of surface/map
category (str): For horizons/zones only: e.g. 'DS_extracted'.
stype (str): RMS folder type, 'horizons' (default), 'zones' or 'clipboard'
category (str): Required for horizons/zones: e.g. 'DS_extracted'. For
clipboard/general2d_data is reperesent the folder(s), where "" or None
means no folder, while e.g. "myfolder/subfolder" means that folders
myfolder/subfolder will be created if not already present. For
stype = 'trends', the category will not be applied
stype (str): RMS folder type, 'horizons' (default), 'zones', 'clipboard'
'general2d_data', 'trends'
realisation (int): Realisation number, default is 0
Raises:
Expand All @@ -1447,17 +1452,19 @@ def to_roxar(
# store in project
topupperreek.to_roxar(project, 'TopUpperReek', 'DS_something')
.. versionadded:: 2.1 clipboard support
Note::
When dealing with surfaces to and from ``stype="trends"``, the surface must
exist in advance, i.e. the Roxar API do not allow creating new surfaces.
Actually trends are read only, but a workaround using ``load()`` in Roxar
API makes it possible to overwrite existing surface trends. In addition,
``realisation`` is not applied in trends.
"""
stype = stype.lower()
valid_stypes = ["horizons", "zones", "clipboard"]
if stype in valid_stypes and name is None or category is None:
logger.error("Need to spesify name and category for " "horizon")
elif stype not in valid_stypes:
raise ValueError("Only {} stype is supported per now".format(valid_stypes))
.. versionadded:: 2.1 clipboard support
.. versionadded:: 2.19 general2d_data and trends support
"""
_regsurf_roxapi.export_horizon_roxapi(
self, project, name, category, stype, realisation
)
Expand Down
Loading

0 comments on commit 547d39d

Please sign in to comment.