The qcforward functions

The qcforward class provides functionality (methods) to check the result of various issues during an ensemble run.

All methods share a design philosopy

  • The client (user) scripts shall be small and simple and easy to use also for modellers with little Python experience.

  • Input will be a python dictionary, or a YAML file

  • If possible (and within scope of method), the qcforward methods should be possible to run both inside RMS and outside RMS.

  • Exception of the previous bullet point may occur e.g. if an Eclipse initialisation is required first; then running the qcforward job outside RMS is logical.

  • All methods shall have a similar appearance (… as similar as possible)

Methods:

Compare well zonation and grid

This method check how the zonelog and/or a perforation log matches with zonation in the 3D grid. If worse than a given set of limits, either are warning is given or a full stop of the workflow is forced.

<https://xtgeo.readthedocs.io/en/latest/_images/zone-well-mismatch-plain.svg>

The input to this method is a python dictionary with some defined keys. Note that the order of keys does not matter.

Common fields (same input inside or outside RMS)

verbosity

Level of output while running None, “info” or “debug”, default is None. (optional)

zonelog:

A dictionary with keys name, range and shift, see examples (required)

perflog:

The name of the perforation log. A dictionary with keys name, range, see examples (optional). If present, zonelog matching will be performed only in perforation intervals.

well_resample:

To speed up calulations (but on cost of less precision), the wells are resampled every N.n units along the well path. E.g. the value of 3.0 means every 3 meter if a metric unit system.

depthrange

A list with two entries, defining minimum and maximum depth to use (both ends are inclusive). Default is [0, 9999]. Setting this range to reservoir gross interval (e.g. [2200, 3400] will speed up calculations, so it is recommended.

actions

This is a list of dictionaries that shows what actions which shall be performed per well (any) or for the average (all), for example {"warn": "all < 50", "stop": "all < 30"} which means that match < than 50% will trigger a warning, while a match < 30% will trigger a stop in work flow. Note that any and all shall not be mixed in the same record (required).

report

Result will be written in a CSV file (which e.g. can be used in plotting) on disk. (optional)

dump_yaml

If present, should be a file name where the current data structure is dumped to YAML format. Later this YAML file can be edited and applied for a single line input

nametag

A string to identify the data set. Recommended.

Keys if ran inside RMS

wells

In RMS this is a dictionary with 3 fields: names, logrun and trajectory. The names is a list of wellnames which in turn can have python valid regular expressions. See examples. (required)

grid

Name of grid icon in RMS (required)

gridprops

A list of grid properties, in this case the name of zone icon in RMS (required)

If ran in normal python (terminal or ERT job)

wells

Outside RMS, wells is a list of files on RMS ascii well format. File wildcards are allowed, se example. (required)

grid

Name of file with grid (on ROFF or EGRID or GRDECL format) (required)

gridprops

A list of list where the inner list is a pair with name of Zone and assosiated filename, for example [["Zone", "zone.roff"]]

Known issues

  • The code evaluates of a well sample is inside a grid cell. However, such evaluation is non-unique as corner point cells are not necessarly well defined in 3D. Hence one may encounter different results if another tool is applied.

Examples

Example when ran inside RMS

from fmu.tools import qcforward

# will match all wells starting with 33_10 and all 34_11 wells containing "A"
# Note that these are python regular expressions!
WELLS = ["33_10.*", "34_11-.*A.*"]

ZONELOGNAME = "Zonelog"
TRAJ = "Drilled trajectory"
LOGRUN = "log"

GRIDNAME = "SIMGRID"
ZONEGRIDNAME = "Zone"
DRANGE = [2100, 3200]
ZLOGRANGE = [1,3]
ZLOGSHIFT = -1
REPORTPATH = "/tmp/well_vs_grid.csv"

ACT = [
  {"warn": "any < 90", "stop": "any < 70"},
  {"warn": "all < 95", "stop": "all < 80"},
]

QCJOB = qcforward.WellZonationVsGrid()

def check():

    usedata = {
        "wells": {"names": WELLS, "logrun": LOGRUN, "trajectory": TRAJ},
        "zonelog": {"name": ZONELOGNAME, "range": ZLOGRANGE, "shift": ZLOGSHIFT},
        "grid": GRIDNAME,
        "depthrange": DRANGE,
        "gridprops": [ZONEGRIDNAME],
        "actions": ACT,
        "report": REPORTPATH,
        "nametag": "ZONELOG",
    }

    QCJOB.run(usedata, project=project)

if  __name__ == "__main__":
    check()

Example when ran from python script in terminal:

from fmu.tools import qcforward

WPATH = "../output/wells/"

# Here typical linux "file globbing" is used
WELLS = [WPATH + "33_10*.rmswell", WPATH + "34_11-*A*"]
ZONELOGNAME = "Zonelog"
PERFLOGNAME = "PERF"

GRIDNAME = "../output/checks/simgrid.roff"
ZONEGRIDNAME = ["Zone", "../output/checks/simgrid_zone.roff"]

QCJOB = qcforward.WellZonationVsGrid()

def check():

    usedata = {
        "wells": WELLS"
        "grid": GRIDNAME,
        "gridprops": [ZONEGRIDNAME],
        "actions": ACT,
        "report": "../output/qc/well_vs_grid.csv",
    }

    QCJOB.run(usedata)

if  __name__ == "__main__":
    check()

Example in RMS with setting from a YAML file:

from fmu.tools import qcforward as qcf
import yaml

USEDATA = yaml.load("../input/qc/somefile.yml", project=project)

ACT = [
  {"warn": "any < 90", "stop": "any < 70"},
  {"warn": "all < 95", "stop": "all < 80"},
]

def check():
    qcf.wellzonation_vs_grid(USEDATA, project=project)

if  __name__ == "__main__":
    check()

The YAML file may in case look like:

actions:
  - {"warn": "all < 70", "stop": "all < 60"}
depthrange: [1300, 1900]
grid: Mothergrid
gridprops: [Zone]
nametag: TST2
perflog: null
report: {file: chk.csv, mode: write}
verbosity: info
well_resample: 3
wells:
  logrun: log
  names: [31_2-D-1_B.*$]
  trajectory: Drilled trajectory
zonelog:
  name: ZONELOG
  range: [1, 18]
  shift: -1

Example when ran inside RMS with different settings for wells

It may be the case where some wells are less important to match strict than other wells.

import fmu.tools.qcforward as qcf

# will match all wells starting with 33_10 and all 34_11 wells containing "A"
# Note that these are python regular expressions!
WELLS1 = ["33_10.*", "34_11-.*A.*"]
WELLS2 = ["34_11-.*B.*"]


ZONELOGNAME = "Zonelog"
TRAJ = "Drilled trajectory"
LOGRUN = "log"

GRIDNAME = "SIMGRID"
ZONEGRIDNAME = "Zone"

ACT1 = [
  {"warn": "any < 90", "stop": "any < 70"},
  {"warn": "all < 95", "stop": "all < 80"},
]

ACT2 = [
  {"warn": "any < 80", "stop": "any < 70"},
  {"warn": "all < 75", "stop": "all < 60"},
]

ACT_EACH1 = {"warn<": 90, "stop<": 70}
ACT_ALL1 = {"warn<": 95, "stop<": 80}

QCJOB = qcf.WellZonationVsGrid()


def check():

    usedata1 = {
        "wells": {"names": WELLS1, "logrun": LOGRUN, "trajectory": TRAJ},
        "zonelog": {"name": ZONELOGNAME, "range": [1, 5], "shift": -2},
        "grid": GRIDNAME,
        "gridzones": [ZONEGRIDNAME],
        "actions": ACT1,
        "report": {"file": "../output/qc/well_vs_grid.csv", "mode": "write"},
        "nametag": "SET1",
    }

    # make a copy and modify selected items
    usedata2 = usedata1.copy()
    usedata2["wells"]["names"] = WELLS2
    usedata2["actions"] = ACT2,
    usedata2["report"] = {"file": "../output/qc/well_vs_grid.csv", "mode": "append"}
    usedata2["nametag"] = "SET2"

    # note the "reuse" use to avoid duplicate loading of data like the grid
    qcf.wellzonation_vs_grid(usedata1, project=project)
    qcf.wellzonation_vs_grid(usedata2, project=project, reuse=True)

if  __name__ == "__main__":
    check()

Grid quality indicators

This methods checks the grid quality in various ways, similar to the methods RMS use (with some exceptions). If worse than a given set of limits, either are warning is given or a full stop of the workflow is forced.

The input to this method is a python dictionary with some defined keys. Note that the order of keys does not matter.

Grid quality indicators keys

The following qridquality measures are currently supported:

minangle_topbase

Minimum angle per cell for top and base, in degrees.

maxangle_topbase

Maximum angle per cell for top and base, in degrees.

minangle_topbase_proj

Minimum angle per cell for top and base, in degrees, projected in XY view.

maxangle_topbase

Maximum angle per cell for top and base, in degrees, projected in XY view.

minangle_sides

Minimum angle for all side surfaces.

maxangle_sides

Maximum angle for all side surfaces.

collapsed

One or more corners are collapsed in Z.

faulted

Grid cell is faulted (which is very OK in most cases).

negative_thickness

Assign value 1 if cell has negative thickness in one or more corners, 0 else.

concave_proj

Assign value 1 if a cell is concave in projected XY (bird) view, 0 else.

Common fields (same input inside or outside RMS)

verbosity

Level of output while running None, “info” or “debug”, default is None. (optional)

actions

This is a dictionary that shows what actions which shall be performed at well average level. An explanation is given below.

report

Result will be written in a CSV file (which e.g. can be used in plotting) on disk. (optional)

dump_yaml

If present, should be a file name where the current data structure is dumped to YAML format. Later this YAML file can be edited and applied for a single line input

nametag

A string to identify the data set. Recommended.

The actions field explained

The action field in the examples below can be explained likes this:

"minangle_topbase": [{"warn": "all > 1% when < 80", "stop": "all > 1% when < 50"}]

The first warning is triggered if the perecentage of cells which minimum angle is less than than 80 degrees, is greater than 1%. Note that:

  • The first word must contain all or any, e.g. allcells will also work

  • There must be spaces between words as shown in example above

  • The use of % is not required, e.g. "all > 1 when < 80" will also work

  • The when word can be replaces with e.g. if or given; the important issue is that a single word is present

Keys if ran inside RMS

grid

Name of grid icon in RMS (required)

writeicon:

If inside RMS will write an icon under the given grid if True.

If ran in normal python (terminal or ERT job)

grid

Name of file with grid (on ROFF or EGRID or GRDECL format) (required)

Known issues

  • Not all RMS grid quality indicators are currently present.

Examples

Example when ran inside RMS

from fmu.tools import qcforward

GRIDNAME = "SIMGRID"

ACTIONS = {
    "minangle_topbase": [
        {"warn": "allcells > 1% when < 80", "stop": "allcells > 1% when < 50"},
        {"warn": "allcells > 50% when < 85", "stop": "all > 10% when < 50"},
        {"warn": "allcells > 50% when < 85"},
    ],
    "collapsed": [{"warn": "all > 20%", "stop": "all > 50%"}],
    "faulted": [{"warn": "all > 20%", "stop": "all > 50%"}],
}

QCJOB = qcforward.GridQuality()

def check():

    usedata = {
        "grid": GRIDNAME,
        "actions": ACTIONS,
        "report": {"file": "../output/qc/gridquality.csv", "mode": "write"},
        "nametag": "ZONELOG",
    }

    QCJOB.run(usedata, project=project)

if  __name__ == "__main__":
    check()

Example when ran from python script in terminal:

from fmu.tools import qcforward


GRIDNAME = "../output/checks/simgrid.roff"
ZONEGRIDNAME = ["Zone", "../output/checks/simgrid_zone.roff"]

QCJOB = qcforward.GridQuality()

def check():

    usedata = {
        "grid": GRIDNAME,
        "actions": ACTIONS,
        "report": {"file": "../output/qc/gridquality.csv", "mode": "write"}
    }

    QCJOB.run(usedata)

if  __name__ == "__main__":
    check()

Example in RMS with setting from a YAML file:

from fmu.tools import qcforward as qcf
import yaml

USEDATA = yaml.load("../input/qc/gridquality.yml", project=project)

def check():
    qcf.wellzonation_vs_grid(USEDATA, project=project)

if  __name__ == "__main__":
    check()

Compare blocked wells with corresponding grid properties

This method checks how the blocked well cell properties match with corresponding grid properties in the 3D grid. If worse than a given set of limits, either a warning is given or a full stop of the workflow is forced.

The input to this method is a python dictionary with some defined keys. Note that the order of keys does not matter. It is advised to study the examples further below.

Common fields (same input inside or outside RMS)

verbosity

Level of output while running None, “info” or “debug”, default is None (optional).

compare:

A dictionary on the form {"Facies": "FACIES", "PHIT": "POROSITY"} where the first elements are the property name in blocked wells (here "Facies, "PHIT") and the second elements (after the colon) are the corresponding name in the grid properties (here "FACIES", "POROSITY"). Note that names can of course be equal.

actions

This is a list of dictionaries that shows what actions which shall be performed per well (any) or for the average (all), for example {"warn": "all < 50", "stop": "all < 30"} which means that match < than 50% will trigger a warning, while a match < 30% will trigger a stop in work flow. Note that any and all shall not be mixed in the same record (required). Also both lines are (currently) required. Finaly note that these are all equivalent:

{"warn": "all < 50", "stop": "all <30"}
{"warn": "allwells < 50", "stop": "allwells <30"}
{"warn": "allfoo < 50", "stop": "allfoo <30"}

Hence it is the substrings all or any that matters.

report

Result will be written in a CSV file (which e.g. can be used in plotting) on disk. (optional).

dump_yaml

If present, should be a file name where the current data structure is dumped to YAML format. Later this YAML file can be edited and applied for a single line input (optional).

nametag

A string to identify the data set. Recommended.

tolerance

Provide the tolerance for the match between a blocked well cell and the corresponding grid property cell. This tolerance can be given either as an absolute tolerance or a relative tolerance. An asolute tolerance can be given on the form 0.01 or as a dictionary: {"abs": 0.01} while a relative tolerance shall be given as {"rel": 0.01}. An absolute tolerance just looks at the absolute value of the difference, while a relative tolerance will do a weighting on the mean value from the blocked logs, and is thus more sensible to use for input logs with varying average values (e.g. porosity and permeability).

show_data

A string or a dict (or None) if the processed dataset shall be shown to screen as a dataframe (table). This can be useful when investigating what wells that trigger e.g. a stop. Possible values are None (default, means no output), “yes” just to show all. However for a more fine-grained control, using a dict is recommended:

# show FAIL lines for wells that have status STOP:
"show_data": {"lines": "FAIL", "wellstatus": "stop"}

# show FAIL lines for wells that have status WARN:
"show_data": {"lines": "FAIL", "wellstatus": "warn"}
tvd_range

A list of two numbers can be applied to limit the vertical range of the comparison. The numbers shall represent minimum and maximum depth, and the range is inclusive, e.g.:

"tvd_range" [1200, 2300]

Note that this setting can exclude whole wells from being evaluated.

Keys if run inside RMS

bwells

In RMS this is a dictionary with 3 fields: names, grid and bwname. The names is a list of wellnames which in turn can have python valid regular expressions. See examples. (required).

grid

Name of grid icon in RMS (required).

If run in normal python (terminal or ERT job)

bwells

Outside RMS, wells is a list of files on RMS ascii well format. File wildcards are allowed, se example. (required).

grid

Name of file with grid (in ROFF, EGRID or GRDECL format) (required).

gridprops

A list of list where the inner list is a pair with name of Zone and assosiated filename, for example [["Zone", "zone.roff"]]. This is required when running outside RMS but not needed when run inside RMS.

Example when run inside RMS

from pathlib import Path
import fmu.tools

GNAME = "Geogrid_Valysar"

# dict showing "mappings", blocked well version first
COMPARE1 = {"Facies": "FACIES", "PHIT": "PHIT"}

BWDATA = {
    "names": ["55.*"],
    "grid": GNAME,
    "bwname": "BW",
}

REPORTPATH1 = "../output/qc/bw_vs_prop.csv"

ACTIONS1 = [
    {"warn": "any < 95%", "stop": "any < 80%"},
    {"warn": "all < 95%", "stop": "all < 90%"},
]

QCJOB = fmu.tools.qcforward.BlockedWellsVsGridProperties()


def check():

    # make report folder if not present
    reportpath = Path(REPORTPATH1)
    reportpath.parent.mkdir(parents=True, exist_ok=True)

    usezonedata = {
        "bwells": BWDATA,
        "grid": GNAME,
        "actions": ACTIONS1,
        "report": REPORTPATH1,
        "compare": COMPARE1,
        "verbosity": "info",
        "tolerance": {"rel": 0.1},
        "nametag": "BWCHECK",
        "show_data": {"lines": "FAIL", "wellstatus": "stop"},
        "tvd_range": [0, 2700],
    }

    QCJOB.run(usezonedata, project=project)


if __name__ == "__main__":
    check()

Example when run from python script in terminal

from pathlib import Path
import fmu.tools

GFILE = "../output/qc/grid/geogrid_valysar.roff"  # has both grid and props
BWELLFILES = ["../qc/output/wells/bw/valysar*.bw"]

# dict showing "mappings", blocked well version first
COMPARE1 = {"Facies": "FACIES", "PHIT": "PHIT"}

GPROPS = [["FACIES", GFILE], ["PHIT", GFILE]]

REPORTPATH1 = "../output/qc/bw_vs_prop.csv"

ACTIONS1 = [
    {"warn": "any < 95%", "stop": "any < 80%"},
    {"warn": "all < 95%", "stop": "all < 90%"},
]

QCJOB = fmu.tools.qcforward.BlockedWellsVsGridProperties()

def check():

    # make report folder if not present
    reportpath = Path(REPORTPATH1)
    reportpath.parent.mkdir(parents=True, exist_ok=True)

    usezonedata = {
        "bwells": BWELLFILES,
        "grid": GFILE,
        "gridprops": GPROPS,
        "actions": ACTIONS1,
        "report": REPORTPATH1,
        "compare": COMPARE1,
        "verbosity": "info",
        "tolerance": {"rel": 0.1},
        "nametag": "BWCHECK",
        "show_data": "yes",
    }

    QCJOB.run(usezonedata, project=project)


if __name__ == "__main__":
    check()

Running grid_statistics

This method checks if property statistics from 3D grids are within user specified thresholds. If worse than a given set of limits, either a warning is given or a full stop of the workflow is forced.

Both discrete and continous properties are supported.

Signature

The input to this method is a python dictionary with some defined keys. Note that the order of keys does not matter.

Required keys

grid

Name of grid icon if run inside RMS, or name of file with grid (on ROFF or EGRID or GRDECL format)

actions

This is a list of dictionaries. Each dictionary specifies a condition to check statistics for, and what action should be performed if outside a given thresholds (either warn or stop the workflow).

Input keys:

property

Name of property (either a property icon in RMS, or a file name)

codename

The discrete property code name to check value for (optional). .. note:: A codename is only needed for discrete properties

calculation

Name of statistical value to check (optional). Default option is “Avg”, while other valid options for continous properties are “Min”, “Max” and “Stddev”.

selectors

A dictionary of conditions to extract statistics from. e.g. a specific zone and/or region (optional).

The key is the name of the property (either a property icon in RMS, or a file name), and the value is the code name.

filters

A dictionary of filters (optional). The key is the name (or path) to the filter parameter, and the value is a dictionary with options “include” or “exclude” where key are the list of values to include/exclude. Only discrete parameters are supported.

For example {"ZONE": {"include: ["Zone_1", "Zone_2"]}}

stop_outside

This is a list with two values which defines the minimum and maximum threshold for when to trigger a stop of the workflow (required).

For example [0.05, 0.35] will give a warning if the statistic is < than 0.05 and > than 0.35.

Note

For discrete properties the statistical value will be reported in fractions.

warn_outside

Same as warn_outside key above, but instead defines when to give a warning (optional).

description

A string to describe each action (optional).

Optional fields

path

Path to grid property files and grid if run outside RMS (optional)

verbosity

Level of output while running None, “info” or “debug” (optional). Default is None.

report

Name of CSV file to write results to (optional)

dump_yaml

File name where the current data structure is dumped to YAML format (optional). Later this YAML file can be edited and applied for a single line input

nametag

A string to identify the data set (optional). Recommended.

Examples

Example when executed inside RMS (continous properties - basic):

from fmu.tools import qcforward as qcf

# Check average grid statistics for porosity and permeability

GRIDNAME = "SimGrid"
REPORT = "somefile.csv"
ACTIONS = [
    {
        "property": "PORO",
        "warn_outside": [0.10, 0.25],
        "stop_outside": [0.05, 0.35],
    },
    {
        "property": "PERM",
        "stop_outside": [100, 2000],
    },
]

def check():

    usedata = {
        "grid": GRIDNAME,
        "actions": ACTIONS,
        "report": REPORT,
    }

    qcf.grid_statistics(usedata, project=project)

if  __name__ == "__main__":
    check()

Example when executed inside RMS (discrete properties - basic):

from fmu.tools import qcforward as qcf

# Check average grid statistics for porosity and permeability

GRIDNAME = "SimGrid"
REPORT = "somefile.csv"
ACTIONS = [
    {
        "property": "Facies",
        "codename": "Sand",
        "selectors": {"Zone": "Top_Zone"},
        "stop_outside": [0.4, 0.8],
    },
    {
        "property": "Facies",
        "codename": "Sand",
        "selectors": {"Zone": "Mid_Zone"},
        "stop_outside": [0.2, 0.5],
    },
]

def check():

    usedata = {
        "grid": GRIDNAME,
        "actions": ACTIONS,
        "report": REPORT,
    }

    qcf.grid_statistics(usedata, project=project)

if  __name__ == "__main__":
    check()

Example when executed inside RMS (continous properties - more settings):

from fmu.tools import qcforward as qcf

# Check average grid statistics for the porosity in HC-zone
# Separate checks for the different zones

GRIDNAME = "SimGrid"
REPORT = "somefile.csv"

ZONE_STOPS = {
    "Top_Zone": [0.05, 0.25],
    "Mid_Zone": [0.15, 0.4],
    "Bottom_Zone": [0.1, 0.3],
}

def check():

    actions = []
    for zone, limits in ZONE_STOPS.items():
        actions.append(
            {
                "property": "PORO",
                "selectors": {"Zone": zone},
                "filters": {"FLUID": {"include": ["Gas", "Oil"]}},
                "stop_outside": limits,
            },
        )

    usedata = {
        "nametag": "MYDATA1",
        "grid": GRIDNAME,
        "report": REPORT,
        "actions": actions,
    }

    qcf.grid_statistics(usedata, project=project)

if  __name__ == "__main__":
    check()

Example when executed from python script in terminal:

from fmu.tools import qcforward as qcf

# Check average grid statistics for a porosity

PATH = "../output/checks/"
GRIDNAME = "simgrid.roff"
REPORT = "somefile.csv"

ACTIONS = [
    {
        "property": "poro.roff",
        "selectors": {"zone.roff": "Top_Zone"},
        "warn_outside": [0.10, 0.25],
        "stop_outside": [0.05, 0.35],
    },
]

def check():

    usedata = {
        path: PATH,
        grid: GRIDNAME,
        actions: ACTIONS,
        report: REPORT,
    }

    qcf.grid_statistics(usedata)

if  __name__ == "__main__":
    check()

Example in RMS with setting from a YAML file:

from fmu.tools import qcforward as qcf
import yaml

USEDATA = yaml.load("../input/qc/somefile.yml", project=project)

def check():
    qcf.grid_statistics(USEDATA, project=project)

if  __name__ == "__main__":
    check()

The YAML file may in case look like:

grid: Mothergrid
actions:
- property: PORO
  stop_outside: [0, 1]
  warn_outside: [0.18, 0.25]
- property: PORO
  selectors:
    Zone: Top_Zone
  filters:
    REGION:
      exclude: ["Surroundings"]
  stop_outside: [0, 1]
  warn_outside: [0.18, 0.25]
report: somefile.csv
nametag: QC_PORO
verbosity: info