Quick start#

Welcome to the Quick start guide! This should help you get started using welly.

First some preliminaries…

import numpy as np
import matplotlib.pyplot as plt

import welly
welly.__version__
'0.5.2'

Create a project from LAS#

Use the read_las() function to load a well by passing a single filename, a POSIX-style path with wildcards (e.g. 'file_*.las') or URL as a str.

This creates a project containing one (or more, depending on the wildcard) wells.

project = welly.read_las('https://geocomp.s3.amazonaws.com/data/P-129.LAS')

project
0it [00:00, ?it/s]
1it [00:01,  1.90s/it]
1it [00:01,  1.90s/it]

IndexUWIDataCurves
0Long = 63* 45'24.460 W24 curvesCALI, HCAL, PEF, DT, DTS, DPHI_SAN, DPHI_LIM, DPHI_DOL, NPHI_SAN, NPHI_LIM, NPHI_DOL, RLA5, RLA3, RLA4, RLA1, RLA2, RXOZ, RXO_HRLT, RT_HRLT, RM_HRLT, DRHO, RHOB, GR, SP

Clearly there are some issues with this well. Have a look at the Well object docs to see how to fix them.

You can index into a project to get at a single well.

well = project[0]

well
Kennetcook #2
Long = 63* 45'24.460 W
crsCRS({})
locationLat = 45* 12' 34.237" N
countryCA
provinceNova Scotia
latitude
longitude
datum
section45.20 Deg N
rangePD 176
township63.75 Deg W
ekb94.8
egl90.3
gl90.3
tdd1935.0
tdl1935.0
tdNone
dataCALI, DPHI_DOL, DPHI_LIM, DPHI_SAN, DRHO, DT, DTS, GR, HCAL, NPHI_DOL, NPHI_LIM, NPHI_SAN, PEF, RHOB, RLA1, RLA2, RLA3, RLA4, RLA5, RM_HRLT, RT_HRLT, RXOZ, RXO_HRLT, SP

The well’s header contains the well information from the WELL part of the file.

PLEASE NOTE The header attribute is under active development and will change in the next release of welly. We will make it more natural to find basic information about the well, without having to know how to read an LAS file.

well.header
original_mnemonic mnemonic unit value descr section
0 VERS VERS 2.0 Version
1 WRAP WRAP YES Version
2 STRT STRT M 1.0668 START DEPTH Well
3 STOP STOP M 1939.1376 STOP DEPTH Well
4 STEP STEP M 0.1524 STEP Well
... ... ... ... ... ... ...
137 TLI TLI M 280.0 Top Log Interval Parameter
138 UWID UWID Unique Well Identification Number Parameter
139 WN WN Kennetcook #2 Well Name Parameter
140 EPD EPD M 90.300003 Elevation of Permanent Datum above Mean Sea Level Parameter
141 UNKNOWN Other

142 rows × 6 columns

The well’s location contains the location info from PARAMS:

well.location
Location({'position': None, 'crs': CRS({}), 'location': 'Lat = 45* 12\' 34.237" N', 'country': 'CA', 'province': 'Nova Scotia', 'latitude': '', 'longitude': '', 'datum': '', 'section': '45.20 Deg N', 'range': 'PD 176', 'township': '63.75 Deg W', 'ekb': 94.8, 'egl': 90.3, 'gl': 90.3, 'tdd': 1935.0, 'tdl': 1935.0, 'td': None, 'deviation': None})

Basic plots#

Welly produces simple matplotlib plots. You can add your own Matplotlib code to build on them.

Let’s plot some important logs. We can put two logs in the same track, as shown, and add depth tracks:

tracks = ['MD', 'CALI', 'GR', 'SP', 'RHOB', ['DT', 'DTS'], 'MD']

well.plot(tracks=tracks)
../_images/Quick_start_11_0.png

Curve data#

The well curves are stored in well.data, which is a dictionary mapping mnemonic to welly.Curve object:

well.data
{'CALI': Curve(mnemonic=CALI, units=in, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'HCAL': Curve(mnemonic=HCAL, units=in, start=1.0668, stop=1939.1376, step=0.1524, count=[2139]),
 'PEF': Curve(mnemonic=PEF, units=, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'DT': Curve(mnemonic=DT, units=us/ft, start=1.0668, stop=1939.1376, step=0.1524, count=[10850]),
 'DTS': Curve(mnemonic=DTS, units=us/ft, start=1.0668, stop=1939.1376, step=0.1524, count=[10850]),
 'DPHI_SAN': Curve(mnemonic=DPHI_SAN, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'DPHI_LIM': Curve(mnemonic=DPHI_LIM, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'DPHI_DOL': Curve(mnemonic=DPHI_DOL, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'NPHI_SAN': Curve(mnemonic=NPHI_SAN, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'NPHI_LIM': Curve(mnemonic=NPHI_LIM, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'NPHI_DOL': Curve(mnemonic=NPHI_DOL, units=m3/m3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RLA5': Curve(mnemonic=RLA5, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RLA3': Curve(mnemonic=RLA3, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RLA4': Curve(mnemonic=RLA4, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RLA1': Curve(mnemonic=RLA1, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RLA2': Curve(mnemonic=RLA2, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RXOZ': Curve(mnemonic=RXOZ, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RXO_HRLT': Curve(mnemonic=RXO_HRLT, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[2139]),
 'RT_HRLT': Curve(mnemonic=RT_HRLT, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RM_HRLT': Curve(mnemonic=RM_HRLT, units=ohm.m, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'DRHO': Curve(mnemonic=DRHO, units=g/cm3, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'RHOB': Curve(mnemonic=RHOB, units=g/cm3, start=1.0668, stop=1939.1376, step=0.1524, count=[12707]),
 'GR': Curve(mnemonic=GR, units=gAPI, start=1.0668, stop=1939.1376, step=0.1524, count=[12718]),
 'SP': Curve(mnemonic=SP, units=mV, start=1.0668, stop=1939.1376, step=0.1524, count=[12718])}

Let’s assign gr to the gamma-ray log so we can inspect it more easily:

gr = well.data['GR']

gr
GR [gAPI]
1.0668 : 1939.1376 : 0.1524
index_unitsm
codeNone
descriptionGamma-Ray
log_typeNone
apiNone
date10-Oct-2007
null-999.25
run1
service_companySchlumberger
_aliasGR
Stats
samples (NaNs)12718 (0)
min mean max3.89 78.986 267.94
DepthValue
1.066846.6987
1.219246.6987
1.371646.6987
1938.832892.2462
1938.985292.2462
1939.137692.2462

It’s often helpful to make a quick plot:

gr.plot()
<AxesSubplot:title={'center':'GR'}, xlabel='gAPI'>
../_images/Quick_start_17_1.png

Curve data as a pandas.DataFrame#

The df attribute (attributes are like ordinary variables; they do not have parentheses after them) of a single curve is a dataframe:

gr.df
GR
DEPT
1.0668 46.69865036
1.2192 46.69865036
1.3716 46.69865036
1.5240 46.69865036
1.6764 46.69865036
... ...
1938.5280 92.24622345
1938.6804 92.24622345
1938.8328 92.24622345
1938.9852 92.24622345
1939.1376 92.24622345

12718 rows × 1 columns

And you can get at all of the well data in a similar way, except that this is a method not an attribute.

well.df()
CALI HCAL PEF DT DTS DPHI_SAN DPHI_LIM DPHI_DOL NPHI_SAN NPHI_LIM ... RLA1 RLA2 RXOZ RXO_HRLT RT_HRLT RM_HRLT DRHO RHOB GR SP
DEPT
1.0668 2.4438154697 4.3912849426 3.5864000320 NaN NaN 0.1574800014 0.1984400004 0.2590999901 0.4650999904 0.3364700079 ... 0.0320999995 0.0279399995 0.0576100014 0.0255800001 0.0255800001 0.0550099984 0.1942329407 2.3901498318 46.69865036 120.1250
1.2192 2.4438154697 4.3912849426 3.5864000320 NaN NaN 0.1574800014 0.1984400004 0.2590999901 0.4650999904 0.3364700079 ... 0.0320999995 0.0279399995 0.0576100014 0.0255800001 0.0255800001 0.0550099984 0.1942329407 2.3901498318 46.69865036 120.1250
1.3716 2.4438154697 4.3912849426 3.5864000320 NaN NaN 0.1574800014 0.1984400004 0.2590999901 0.4650999904 0.3364700079 ... 0.0320999995 0.0279399995 0.0576100014 0.0255800001 0.0255800001 0.0550099984 0.1942329407 2.3901498318 46.69865036 120.1250
1.5240 2.4438154697 4.3912849426 3.5864000320 NaN NaN 0.1574800014 0.1984400004 0.2590999901 0.4650999904 0.3364700079 ... 0.0320999995 0.0279399995 0.0576100014 0.0255800001 0.0255800001 0.0550099984 0.1942329407 2.3901498318 46.69865036 120.1250
1.6764 2.4438154697 4.3912849426 3.5864000320 NaN NaN 0.1574800014 0.1984400004 0.2590999901 0.4650999904 0.3364700079 ... 0.0320999995 0.0279399995 0.0576100014 0.0255800001 0.0255800001 0.0550099984 0.1942329407 2.3901498318 46.69865036 120.1250
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1938.5280 2.4202680588 NaN 2.2369799614 NaN NaN 0.5464199781 585.9415283200 541.6757202100 0.1283400059 0.0841699988 ... 274.0264892600 663.1040649400 7.1023502350 NaN 7.3863301277 0.0272899996 0.0613951497 NaN 92.24622345 73.4375
1938.6804 2.4202680588 NaN 2.2369799614 NaN NaN 0.5464199781 585.9415283200 541.6757202100 0.1283400059 0.0841699988 ... 274.0264892600 663.1040649400 7.1022100449 NaN 7.3860998154 0.0272899996 0.0613951497 NaN 92.24622345 73.6875
1938.8328 2.4202680588 NaN 2.2369799614 NaN NaN 0.5464199781 585.9415283200 541.6757202100 0.1283400059 0.0841699988 ... 274.0264892600 663.1040649400 7.0968699455 NaN 7.3806500435 0.0272899996 0.0613951497 NaN 92.24622345 73.0000
1938.9852 2.4202680588 NaN 2.2369799614 NaN NaN 0.5464199781 585.9415283200 541.6757202100 0.1283400059 0.0841699988 ... 274.0264892600 663.1040649400 7.0391001701 NaN 7.3211698532 0.0272899996 0.0613951497 NaN 92.24622345 73.9375
1939.1376 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

12718 rows × 24 columns

It has to be a method because we can pass in extra options like aliases.

Curve aliases#

We can make a dictionary mapping alias names (what we want to call curves) to a list of mnemonics (what they are actually called). The list of mnemonics is ordered, and this order expresses your preference. So, for example, in the alias dictionary below, the GR log will be used in preference to the GAM log.

alias = {
    "Gamma": ["GR", "GAM", "GRC", "SGR", "NGT"],
    "Density": ["RHOZ", "RHOB", "DEN", "RHOZ"],
    "Sonic": ["DT", "AC", "DTP", "DT4P"],
    "Caliper": ["CAL", "CALI", "CALS", "C1"],
    'Porosity SS': ['NPSS', 'DPSS'],
}

Lots of methods in welly take an alias. Welly will use the alias dictionary to decide what to show.

Here, the keys argument narrows down the columns to include in the dataframe.

well.df(keys=['Gamma', 'Density', 'Sonic', 'DTS'], alias=alias)
Gamma Density Sonic DTS
DEPT
1.0668 46.69865036 2.3901498318 NaN NaN
1.2192 46.69865036 2.3901498318 NaN NaN
1.3716 46.69865036 2.3901498318 NaN NaN
1.5240 46.69865036 2.3901498318 NaN NaN
1.6764 46.69865036 2.3901498318 NaN NaN
... ... ... ... ...
1938.5280 92.24622345 NaN NaN NaN
1938.6804 92.24622345 NaN NaN NaN
1938.8328 92.24622345 NaN NaN NaN
1938.9852 92.24622345 NaN NaN NaN
1939.1376 NaN NaN NaN NaN

12718 rows × 4 columns

There are a lot of NaNs there, but if we just wanted to see the middle of the well, we can pass in a basis to narrow it down (and even resample the data if we want).

Let’s look at 1000 to 1500 m, resampled to 1 m sample interval:

basis = range(1000, 1501)

well.df(keys=['Gamma', 'Density', 'Sonic', 'DTS'], alias=alias, basis=basis)
Gamma Density Sonic DTS
DEPT
1000.0 109.4141766008 2.6799457361 64.2276356014 106.9954482717
1001.0 97.9911234089 2.6556390146 66.5059106783 114.9005662249
1002.0 121.8829895962 2.7326798533 72.6423786195 128.5069715885
1003.0 116.8162654985 2.7270846667 67.0266704008 114.4374434938
1004.0 104.1488654040 2.7105148702 66.5885520597 112.6149668241
... ... ... ... ...
1496.0 48.4150835474 2.5053586284 57.5187979772 91.2555331735
1497.0 35.7518339679 2.2265663635 59.8886118010 98.4930620567
1498.0 43.7772705495 2.3508678784 59.2961889052 99.9149563203
1499.0 36.5637318164 2.3360053329 63.2269721143 98.0971322843
1500.0 64.5593145777 2.6044978626 53.9417406514 89.4845678370

501 rows × 4 columns

Curve quality#

We can check the quality of curves with a dictionary of tests. There are some predefined tests in the welly.quality module, but you can write your own functions and pass them in instead (the function should take a curve as its argument, and return True if the test passed).

import welly.quality as q

tests = {
    'Each': [
        q.no_flat,
        q.no_monotonic,
        q.no_gaps,
    ],
    'Gamma': [
        q.all_positive,
        q.all_below(450),
        q.check_units(['API', 'GAPI']),
    ],
    'DT': [
        q.all_positive,
    ],
    'Sonic': [
        q.all_between(1, 10000),  # 1333 to 5000 m/s
        q.no_spikes(10),          # 10 spikes allowed
    ],
}
passed = well.qc_data(tests, alias=alias)

This returns a dictionary of curves in which the values are dictionaries of test name: test result pairs.

passed
{'CALI': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'HCAL': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'PEF': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'DT': {'no_flat': True,
  'no_monotonic': True,
  'no_gaps': True,
  'all_positive': True,
  'all_between': True,
  'no_spikes': False},
 'DTS': {'no_flat': True, 'no_monotonic': True, 'no_gaps': True},
 'DPHI_SAN': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'DPHI_LIM': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'DPHI_DOL': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'NPHI_SAN': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'NPHI_LIM': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'NPHI_DOL': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RLA5': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RLA3': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RLA4': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RLA1': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RLA2': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RXOZ': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RXO_HRLT': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RT_HRLT': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RM_HRLT': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'DRHO': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'RHOB': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True},
 'GR': {'no_flat': False,
  'no_monotonic': False,
  'no_gaps': True,
  'all_positive': True,
  'all_below': True,
  'check_units': False},
 'SP': {'no_flat': False, 'no_monotonic': False, 'no_gaps': True}}

There’s also an HTML table for rendering in Notebooks:

from IPython.display import HTML

html = well.qc_table_html(tests, alias=alias)
HTML(html)
CurvePassedScoreall_betweenno_spikesall_positiveno_flatcheck_unitsno_gapsall_belowno_monotonic
CALI1 / 30.333FalseTrueFalse
HCAL1 / 30.333FalseTrueFalse
PEF1 / 30.333FalseTrueFalse
DT5 / 60.833TrueFalseTrueTrueTrueTrue
DTS3 / 31.000TrueTrueTrue
DPHI_SAN1 / 30.333FalseTrueFalse
DPHI_LIM1 / 30.333FalseTrueFalse
DPHI_DOL1 / 30.333FalseTrueFalse
NPHI_SAN1 / 30.333FalseTrueFalse
NPHI_LIM1 / 30.333FalseTrueFalse
NPHI_DOL1 / 30.333FalseTrueFalse
RLA51 / 30.333FalseTrueFalse
RLA31 / 30.333FalseTrueFalse
RLA41 / 30.333FalseTrueFalse
RLA11 / 30.333FalseTrueFalse
RLA21 / 30.333FalseTrueFalse
RXOZ1 / 30.333FalseTrueFalse
RXO_HRLT1 / 30.333FalseTrueFalse
RT_HRLT1 / 30.333FalseTrueFalse
RM_HRLT1 / 30.333FalseTrueFalse
DRHO1 / 30.333FalseTrueFalse
RHOB1 / 30.333FalseTrueFalse
GR3 / 60.500TrueFalseFalseTrueTrueFalse
SP1 / 30.333FalseTrueFalse

Add a striplog#

In principle, Welly can hold data from anywhere. Let’s add data from another source. Striplog can create a geological log from an image or a CSV. Let’s try it:

from striplog import Legend, Striplog

legend = Legend.builtin('NSDOE')
strip = Striplog.from_image('https://geocomp.s3.amazonaws.com/data/P-129_280_1935.png', 280, 1935, legend=legend)
strip.plot()
../_images/Quick_start_37_0.png

Add this to the well data:

well.data['strip'] = strip
tracks = ['MD', 'strip', 'GR', 'RHOB', ['DT', 'DTS'], 'MD']

well.plot(tracks=tracks)
../_images/Quick_start_40_0.png

Write a LAS file#

At any point, you can write out a new LAS file.

Let’s write a new file with only the DT and DTS logs, and a different NULL value to the original:

well.to_las('out.las', keys=['DT', 'DTS'], null_value=-111.111)

You can perform a shell command in Jupyter Notebooks by putting a ! before the command. So on Linux, here’s how we can check the new file exists:

!ls -l out.las
-rw-r--r-- 1 runner docker 434247 Feb 28 13:55 out.las
!head -12 out.las
~Version ---------------------------------------------------
VERS.   2.0 : CWLS log ASCII Standard -VERSION 2.0
WRAP.    NO : One line per depth step
DLM . SPACE : Column Data Section Delimiter
~Well ------------------------------------------------------
STRT  .m                     1.0668 : START DEPTH
STOP  .m                  1939.1376 : STOP DEPTH
STEP  .m                    0.15240 : STEP
NULL  .                    -111.111 : NULL VALUE
COMP  . Elmworth Energy Corporation : COMPANY
WELL  .               Kennetcook #2 : WELL
FLD   .               Windsor Block : FIELD

© 2022 Agile Scientific, CC BY