Skip to content

Commit

Permalink
ENH: add key update_option for Well to roxar
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrivenaes committed Nov 19, 2023
1 parent dc6c431 commit 7e1cc57
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 86 deletions.
168 changes: 90 additions & 78 deletions src/xtgeo/well/_well_roxapi.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
"""Well input and output, private module for ROXAPI."""

from __future__ import annotations

from typing import Any, Literal, Optional

import numpy as np
import numpy.ma as npma
import pandas as pd

import xtgeo
from xtgeo.common import XTGeoDialog
from xtgeo.common._xyz_enum import _AttrName, _AttrType
from xtgeo.common.constants import UNDEF_INT_LIMIT, UNDEF_LIMIT
Expand Down Expand Up @@ -149,87 +154,116 @@ def _get_roxlog(wlogtypes, wlogrecords, roxlrun, lname): # pragma: no cover


def export_well_roxapi(
self,
self: xtgeo.Well,
project,
wname,
lognames="all",
logrun="log",
trajectory="Drilled trajectory",
realisation=0,
lognames: str | list[str] = "all",
logrun: str = "log",
trajectory: str = "Drilled trajectory",
realisation: int = 0,
update_option: Optional[Literal["overwrite", "append"]] = None,
):
"""Private function for well export (store in RMS) from XTGeo to RoxarAPI."""
"""Private function for well export (i.e. store in RMS) from XTGeo to RoxarAPI."""
logger.debug("Opening RMS project ...")

rox = RoxUtils(project, readonly=False)

_roxapi_export_well(self, rox, wname, lognames, logrun, trajectory, realisation)
if wname in rox.project.wells:
_roxapi_update_well(
self, rox, wname, lognames, logrun, trajectory, realisation, update_option
)
else:
_roxapi_create_well(self, rox, wname, lognames, logrun, trajectory, realisation)

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

rox.safe_close()


def _roxapi_export_well(self, rox, wname, lognames, logrun, trajectory, realisation):
if wname in rox.project.wells:
_roxapi_update_well(self, rox, wname, lognames, logrun, trajectory, realisation)
else:
_roxapi_create_well(self, rox, wname, lognames, logrun, trajectory, realisation)
def _store_log_in_roxapi(self, lrun: Any, logname: str) -> None:
"""Store a single log in RMS / Roxar API for a well"""
if logname in (self.xname, self.yname, self.zname):
return

isdiscrete = False
xtglimit = UNDEF_LIMIT
if self.wlogtypes[logname] == _AttrType.DISC.value:
isdiscrete = True
xtglimit = UNDEF_INT_LIMIT

def _roxapi_update_well(self, rox, wname, lognames, logrun, trajectory, realisation):
"""Assume well is to updated only with logs, new or changed.
store_logname = logname

Also, the length of arrays should not change, at least not for now.
# the MD name is applied as default in RMS for measured depth; hence it can be wise
# to avoid duplicate names here, since the measured depth log is crucial.
if logname == "MD":
store_logname = "MD_imported"
xtg.warn(f"Logname MD is stored as {store_logname}")

"""
logger.debug("Key realisation not in use: %s", realisation)
if isdiscrete:
thelog = lrun.log_curves.create_discrete(name=store_logname)
else:
thelog = lrun.log_curves.create(name=store_logname)

well = rox.project.wells[wname]
traj = well.wellbore.trajectories[trajectory]
lrun = traj.log_runs[logrun]
values = thelog.generate_values()

lrun.log_curves.clear()
if values.size != self.dataframe[logname].values.size:
raise ValueError("New logs have different sampling or size, not possible")

if lognames == "all":
uselognames = self.lognames
else:
uselognames = lognames
usedtype = values.dtype

for lname in uselognames:
isdiscrete = False
xtglimit = UNDEF_LIMIT
if self.wlogtypes[lname] == _AttrType.DISC.value:
isdiscrete = True
xtglimit = UNDEF_INT_LIMIT

if isdiscrete:
thelog = lrun.log_curves.create_discrete(name=lname)
vals = np.ma.masked_invalid(self.dataframe[logname].values)
vals = np.ma.masked_greater(vals, xtglimit)
vals = vals.astype(usedtype)
thelog.set_values(vals)

if isdiscrete:
# roxarapi requires keys to be ints, while xtgeo can accept any, e.g. strings
if vals.mask.all():
codedict = {0: "unset"}
else:
thelog = lrun.log_curves.create(name=lname)
codedict = {
int(key): str(value) for key, value in self.wlogrecords[logname].items()
}
thelog.set_code_names(codedict)


def _roxapi_update_well(
self: xtgeo.Well,
rox: Any,
wname: str,
lognames: str | list[str],
logrun: str,
trajectory: str,
realisation: int,
update_option: Optional[Literal["overwrite", "append"]] = None,
):
"""Assume well is to updated only with logs, new only are appended
values = thelog.generate_values()
Also, the length of arrays are not allowed not change (at least not for now).
"""
logger.debug("Key realisation not in use: %s", realisation)
if update_option not in (None, "overwrite", "append"):
raise ValueError(
f"The update_option <{update_option}> is invalid, valid "
"options are: None | overwrite | append"
)

if values.size != self.dataframe[lname].values.size:
raise ValueError("New logs have different sampling or size, not possible")
lrun = rox.project.wells[wname].wellbore.trajectories[trajectory].log_runs[logrun]

usedtype = values.dtype
# find existing lognames in target
current_logs = [lname.name for lname in lrun.log_curves]

vals = np.ma.masked_invalid(self.dataframe[lname].values)
vals = np.ma.masked_greater(vals, xtglimit)
vals = vals.astype(usedtype)
thelog.set_values(vals)
uselognames = self.lognames if lognames == "all" else lognames

if isdiscrete:
# roxarapi requires keys to int, while xtgeo can accept any, e.g. strings
if vals.mask.all():
codedict = {0: "unset"}
else:
codedict = {
int(key): str(value)
for key, value in self._wlogrecords[lname].items()
}
thelog.set_code_names(codedict)
if update_option is None:
lrun.log_curves.clear() # clear existing logs; all will be replaced

for lname in uselognames:
if update_option == "append" and lname in current_logs:
continue
_store_log_in_roxapi(self, lrun, lname)


def _roxapi_create_well(self, rox, wname, lognames, logrun, trajectory, realisation):
Expand Down Expand Up @@ -257,28 +291,6 @@ def _roxapi_create_well(self, rox, wname, lognames, logrun, trajectory, realisat
lrun = traj.log_runs.create(logrun)
lrun.set_measured_depths(md)

# Add log curves
for curvename, curveprop in self.get_wlogs().items():
if curvename not in self.lognames:
continue # skip X_UTME .. Z_TVDSS
if lognames and lognames != "all" and curvename not in lognames:
continue
if not lognames:
continue

cname = curvename
if curvename == "MD":
cname = "MD_imported"
xtg.warn(f"Logname MD is renamed to {cname}")

if curveprop[0] == _AttrType.DISC.value:
lcurve = lrun.log_curves.create_discrete(cname)
cc = np.ma.masked_invalid(self.dataframe[curvename].values)
lcurve.set_values(cc.astype(np.int32))
codedict = {int(key): str(value) for key, value in curveprop[1].items()}
lcurve.set_code_names(codedict)
else:
lcurve = lrun.log_curves.create(cname)
lcurve.set_values(self.dataframe[curvename].values)

logger.debug("Log curve created: %s", cname)
uselognames = self.lognames if lognames == "all" else lognames
for lname in uselognames:
_store_log_in_roxapi(self, lrun, lname)
52 changes: 44 additions & 8 deletions src/xtgeo/well/well1.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,34 +679,69 @@ def to_roxar(self, *args, **kwargs):
The current implementation will either update the existing well
(then well log array size must not change), or it will make a new well in RMS.
Note:
When project is file path (direct access, outside RMS) then
``to_roxar()`` will implicitly do a project save. Otherwise, the project
will not be saved until the user do an explicit project save action.
Args:
project (str, object): Magic string 'project' or file path to project
wname (str): Name of well, as shown in RMS.
lognames (:obj:list or :obj:str): List of lognames to save, or
use simply 'all' for current logs for this well. Default is 'all'
realisation (int): Currently inactive
trajectory (str): Name of trajectory in RMS
logrun (str): Name of logrun in RMS
trajectory (str): Name of trajectory in RMS, default is "Drilled trajectory"
logrun (str): Name of logrun in RMS, defaault is "log"
update_option (str): None | "overwrite" | "append". This only applies
when the well (wname) exists in RMS, and rules are based on name
matching. Default is None which means that all well logs in
RMS are emptied and then replaced with the content from xtgeo.
The "overwrite" option will replace logs in RMS with logs from xtgeo,
and append new if they do not exist in RMS. The
"append" option will only append logs if name does not exist in RMS
already. Reading only a subset of logs and then use "overwrite" or
"append" may speed up execution significantly.
Note:
When project is file path (direct access, outside RMS) then
``to_roxar()`` will implicitly do a project save. Otherwise, the project
will not be saved until the user do an explicit project save action.
Example::
# assume that existing logs in RMS are ["PORO", "PERMH", "GR", "DT", "FAC"]
# read only one existing log (faster)
wll = xtgeo.well_from_roxar(project, "WELL1", lognames=["PORO"])
wll.dataframe["PORO"] += 0.2 # add 0.2 to PORO log
wll.create_log("NEW", value=0.333) # create a new log with constant value
# the "option" is a variable... for output, ``lognames="all"`` is default
if option is None:
# remove all current logs in RMS; only logs will be PORO and NEW
wll.to_roxar(project, "WELL1", update_option=option)
elif option == "overwrite":
# keep all original logs but update PORO and add NEW
wll.to_roxar(project, "WELL1", update_option=option)
elif option == "append":
# keep all original logs as they were (incl. PORO) and add NEW
wll.to_roxar(project, "WELL1", update_option=option)
Note:
The keywords ``lognames`` and ``update_option`` will interact
.. versionadded:: 2.12
.. versionchanged:: 2.15
Saving to new wells enabled (earlier only modifying existing)
.. versionchanged:: 3.5
Add key ``update_option``
"""
# use *args, **kwargs since this method is overrided in blocked_well, and
# signature should be the same
# signature should be the same (TODO: change this to keywords; think this is
# a python 2.7 relict?)

project = args[0]
wname = args[1]
lognames = kwargs.get("lognames", "all")
trajectory = kwargs.get("trajectory", "Drilled trajectory")
logrun = kwargs.get("logrun", "log")
realisation = kwargs.get("realisation", 0)
update_option = kwargs.get("update_option", None)

logger.debug("Not in use: realisation %s", realisation)

Expand All @@ -718,6 +753,7 @@ def to_roxar(self, *args, **kwargs):
trajectory=trajectory,
logrun=logrun,
realisation=realisation,
update_option=update_option,
)

def get_lognames(self):
Expand Down
57 changes: 57 additions & 0 deletions tests/test_roxarapi/test_roxarapi_reek.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,60 @@ def test_rox_well_with_added_logs(roxar_project):
# check that export with set codes
well.set_logrecord("Facies", {1: "name"})
well.to_roxar(roxar_project, "dummy3", logrun="log", trajectory="My trajectory")


@pytest.mark.requires_roxar
@pytest.mark.parametrize(
"update_option, expected_logs, expected_poroavg",
[
(None, ["Poro", "NewPoro"], 0.26376),
("overwrite", ["Zonelog", "Perm", "Poro", "Facies", "NewPoro"], 0.26376),
("append", ["Zonelog", "Perm", "Poro", "Facies", "NewPoro"], 0.16376),
],
)
def test_rox_well_update(roxar_project, update_option, expected_logs, expected_poroavg):
"""Operations on discrete well logs"""
initial_wellname = WELLS1[1].replace(".w", "")
wellname = "TESTWELL"

initial_well = xtgeo.well_from_roxar(
roxar_project,
initial_wellname,
logrun="log",
lognames="all",
trajectory="My trajectory",
)
initial_well.to_roxar(roxar_project, wellname)

print("###############################################")

well = xtgeo.well_from_roxar(
roxar_project,
wellname,
lognames=["Poro"],
)
well.create_log("NewPoro")
well.dataframe["Poro"] += 0.1

well.to_roxar(
roxar_project,
wellname,
lognames=well.lognames,
update_option=update_option,
)
print("Lognames are", well.lognames)

rox = xtgeo.RoxUtils(roxar_project)

rox_lcurves = (
rox.project.wells[wellname]
.wellbore.trajectories["Drilled trajectory"]
.log_runs["log"]
.log_curves
)
rox_lognames = [lname.name for lname in rox_lcurves]
assert rox_lognames == expected_logs

assert rox_lcurves["Poro"].get_values().mean() == pytest.approx(
expected_poroavg, abs=0.001
)

0 comments on commit 7e1cc57

Please sign in to comment.