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
andshift
, 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 thatany
andall
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
andtrajectory
. 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
orany
, e.g.allcells
will also workThere must be spaces between words as shown in example above
The use of
%
is not required, e.g."all > 1 when < 80"
will also workThe
when
word can be replaces with e.g.if
orgiven
; 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 thatany
andall
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
orany
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
andbwname
. 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