Curves

Curves are one of the fundamental objects in welly.

Well objects include collections of Curve objects. Multiple Well objects can be stored in a Project.

On this page, we take a closer look at the Curve object.

Some preliminaries…

import numpy as np
import matplotlib.pyplot as plt

import welly
welly.__version__
'0.5.3rc1'

Load a well from LAS

Use the from_las() method to load a well by passing a filename as a str.

This is really just a wrapper for lasio but instantiates a Header, Curves, etc.

from welly import Well

p129 = Well.from_las('https://geocomp.s3.amazonaws.com/data/P-129.LAS')
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/las.py:485, in file_from_url(url)
    484 try:
--> 485     text_file = StringIO(request.urlopen(url).read().decode())
    486 except error.HTTPError as e:

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:215, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    214     opener = _opener
--> 215 return opener.open(url, data, timeout)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:521, in OpenerDirector.open(self, fullurl, data, timeout)
    520     meth = getattr(processor, meth_name)
--> 521     response = meth(req, response)
    523 return response

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:630, in HTTPErrorProcessor.http_response(self, request, response)
    629 if not (200 <= code < 300):
--> 630     response = self.parent.error(
    631         'http', request, response, code, msg, hdrs)
    633 return response

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:559, in OpenerDirector.error(self, proto, *args)
    558 args = (dict, 'default', 'http_error_default') + orig_args
--> 559 return self._call_chain(*args)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:492, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    491 func = getattr(handler, meth_name)
--> 492 result = func(*args)
    493 if result is not None:

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/urllib/request.py:639, in HTTPDefaultErrorHandler.http_error_default(self, req, fp, code, msg, hdrs)
    638 def http_error_default(self, req, fp, code, msg, hdrs):
--> 639     raise HTTPError(req.full_url, code, msg, hdrs, fp)

HTTPError: HTTP Error 403: Forbidden

During handling of the above exception, another exception occurred:

Exception                                 Traceback (most recent call last)
Cell In[2], line 3
      1 from welly import Well
----> 3 p129 = Well.from_las('https://geocomp.s3.amazonaws.com/data/P-129.LAS')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/well.py:303, in Well.from_las(cls, fname, remap, funcs, data, req, alias, encoding, printfname, index, **kwargs)
    301 # If https URL is passed try reading and formatting it to text file.
    302 if re.match(r'https?://.+\..+/.+?', fname) is not None:
--> 303     fname = file_from_url(fname)
    305 datasets = from_las(fname, encoding=encoding, **kwargs)
    307 # Create well from datasets.

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/las.py:487, in file_from_url(url)
    485     text_file = StringIO(request.urlopen(url).read().decode())
    486 except error.HTTPError as e:
--> 487     raise Exception('Could not retrieve url: ', e)
    489 return text_file

Exception: ('Could not retrieve url: ', <HTTPError 403: 'Forbidden'>)
p129.plot()
../_images/09d64ac50d898f3711f82ff61e983ffab0b3f6d3cadb30e8201087856f10224f.png

The curves are stored in the data attribute, which is an ordinary dictionary:

p129.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 look at one log:

gr = p129.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

The object knows some things about itself:

gr.mnemonic, gr.units, gr.start, gr.stop, gr.step
('GR', 'gAPI', 1.0668, 1939.1376, 0.1524000000000001)

Curves have various methods on them, such as plot()…

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

Often we just want to look at or deal with a portion of the curve, or maybe resample it:

gr.to_basis(start=1000, stop=1250, step=10.0).plot()
<AxesSubplot:title={'center':'GR'}, xlabel='gAPI'>
../_images/1ab3adb3b69790909affbdab4765ab6e81ab7f12443debadb6f5edd2eba86d51.png

Interpolation and slicing

We can read the curve at any depth (or depths) and get an interpolated reading:

gr.read_at([1200, 1300, 1400])
[105.9837716184255, 103.79578067107755, 96.33845173590835]

There are no samples at those depths; the well is sampled at a 0.1524 m interval:

gr.step
0.1524000000000001

The actual depths of the samples are in the ‘index’:

gr.index
Float64Index([   1.0668,    1.2192,    1.3716,     1.524,    1.6764,    1.8288,
                 1.9812,    2.1336,     2.286,    2.4384,
              ...
               1937.766, 1937.9184, 1938.0708, 1938.2232, 1938.3756,  1938.528,
              1938.6804, 1938.8328, 1938.9852, 1939.1376],
             dtype='float64', name='DEPT', length=12718)

You can slice a curve by this index; in other words, by depth:

gr[1000:1010]
GR [gAPI]
1000.0488 : 1009.9548 : 0.1524
index_unitsm
codeNone
descriptionGamma-Ray
log_typeNone
apiNone
date10-Oct-2007
null-999.25
run1
service_companySchlumberger
_aliasGR
Stats
samples (NaNs)66 (0)
min mean max74.87 109.108 134.61
DepthValue
1000.0488111.2134
1000.2012110.6463
1000.3536102.9604
⋮⋮
1009.6500116.1571
1009.8024121.8580
1009.9548126.4625

You can get a statistical description of a curve:

gr.describe()  # Equivalent to get_stats()
GR
count 12718.0000000000
mean 78.9863535888
std 37.0719153332
min 3.8940699100
25% 51.3325605393
50% 76.5569686890
75% 109.8330020900
max 267.9404296900

Mathematics

gr.mean()
GR    78.9863535888
dtype: float64

Mathematical operations results in another Curve object, but the values are transformed:

1000 * p129.data['RHOB']
RHOB [g/cm3]
1.0668 : 1939.1376 : 0.1524
index_unitsm
codeNone
descriptionBulk Density
log_typeNone
apiNone
date10-Oct-2007
null-999.25
run1
service_companySchlumberger
_aliasRHOB
Stats
samples (NaNs)12718 (11)
min mean max1173.95 2613.912 4628.80
DepthValue
1.06682390.1498
1.21922390.1498
1.37162390.1498
⋮⋮
1938.8328nan
1938.9852nan
1939.1376nan

Beware, for the time being, units are not transformed by mathematical operations!

Plotting

gr.plot(c='r', lw=0.5)
<AxesSubplot:title={'center':'GR'}, xlabel='gAPI'>
../_images/811f2eb775da74c9c7ad2c1b3326da07d4f63e632f208b3136e2532753fd8d57.png

There’s also a pseudocolor 2D ribbon plot:

gr.plot_2d()
<AxesSubplot:>
../_images/ab5528a16a5206226e1164d6017ff8da8f91c634fb5b78fe5ddcab58bf9dd5f5.png

You can optionally show the curve trace as well with curve=True:

gr.plot_2d(cmap='viridis_r', curve=True, lw=0.3, edgecolor='k')
plt.xlim(0,200)
(0.0, 200.0)
../_images/797e7ae89f4180024e2ce970b577d69e1a33ca8b705f540804ffb024d9bf1afa.png

Despike

You can despike with a window length for the trend and a Z-score to clip at — the curve is compared to the median in the window using the standard deviation from the entire curve. Here’s the difference:

p129.data['DESP'] = gr.despike(z=1)
p129.data['DIFF'] = gr - p129.data['DESP']
p129.plot(tracks=['GR', 'DESP', 'DIFF'])
../_images/15e4fb402d6faa9be4caac73130408beeabdc1ac94e48e4afb38a9fc635181da.png

Blocking

We can block a curve. Let’s look at a small segment:

segment = gr.to_basis(start=600, stop=680)

We can create a binary log (0’s and 1’s) with a simple cutoff:

fig, axs = plt.subplots(ncols=2)

# The original log on the left.
segment.plot(ax=axs[0], c='r')
axs[0].axvline(80, c='c', alpha=0.7)

# Make and plot a blocked version.
segment.block(cutoffs=80).plot(ax=axs[1])
axs[1].set_xlabel('')
Text(0.5, 0, '')
../_images/4b312197bb4d4b28cb43db5f2a4421fd574ecac81ca3c82e693fcb2a472aeb46.png

Or we can use two cutoffs and get a blocked log with three different values. By default the new values will be 0, 1, 2, but we can assign whatever we like:

fig, ax = plt.subplots()

segment.plot(ax=ax)
segment.block(cutoffs=(80, 120), values=(20, 100, 120)).plot(ax=ax)
<AxesSubplot:title={'center':'GR'}, xlabel='gAPI'>
../_images/51043a114adb9523bf88017ffefe68d3e6b7478988315bed50f4a732c4c72c6a.png

You can send a function in to determine replacement values from the original log. E.g., to replace the values with the block’s mean value:

fig, ax = plt.subplots()

segment.plot(ax=ax)
segment.block(cutoffs=80, function=np.mean).plot(ax=ax)
plt.axvline(80, color='c', lw=1)
<matplotlib.lines.Line2D at 0x7f0a8e082df0>
../_images/43ac17a749c4307f2d56b3d5cd9f00bf1db91093ec74973afbbab2680dca9952.png

Instantiating a new curve

Let’s add a curve from a list of values (data) with depths (basis):

from welly import Curve

params = {'mnemonic': 'FOO', 'run':0, }
data = [20, 30, 40, 20, 10, 0, 10]
c = Curve(data, index=[2,3,4,5,6,7,8], **params)
c.plot()
<AxesSubplot:title={'center':'FOO'}>
../_images/64353a9e7a17db55adce5f3a5a5798d51b327c8ff4732531fb11303cd6ec4cab.png

© 2022 Agile Scientific, CC BY