# 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.2'


## 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')

p129.plot()


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'>


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'>


## 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'>


There’s also a pseudocolor 2D ribbon plot:

gr.plot_2d()

<AxesSubplot:>


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)


## 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'])


## 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, '')


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'>


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 0x7f5ccda970d0>


## 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'}>


© 2022 Agile Scientific, CC BY