Projects

Wells 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 Project class. It lets us handle groups of wells. It is really just a list of Well objects, with a few extra powers.

First, some preliminaries…

import welly

welly.__version__
'0.5.3rc0'

Make a project

We have a few LAS files in a folder; we can load them all at once with standard POSIX file globbing syntax:

p = welly.read_las("../../tests/assets/example_*.las")
0it [00:00, ?it/s]
/opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/curve.py:515: UserWarning: Index is not evenly sampled. Cannot derive step. Try resampling the curve with, for example `curve.to_basis(step=0.1524)`
  return get_step_from_array(self.df.index.values)
/opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/project.py:183: UserWarning: 3-2-B: Curves have step 0. Consider reampling them with `curve.to_basis(step=0.1524)` or similar.
  wells = [Well.from_las(f,
/opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/curve.py:515: UserWarning: Index is not evenly sampled. Cannot derive step. Try resampling the curve with, for example `curve.to_basis(step=0.1524)`
  return get_step_from_array(self.df.index.values)
/opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/project.py:183: UserWarning: 3-2-A: Curves have step 0. Consider reampling them with `curve.to_basis(step=0.1524)` or similar.
  wells = [Well.from_las(f,

2it [00:00, 19.98it/s]
2it [00:00, 19.86it/s]

Now we have a project, containing two files:

p
IndexUWIDataCurves
03-2-B12 curvesSP, SN, ILD, LLS, LLD, MLL, NPHI, RHOZ, CAL1, GRC, DTP, CAL2
13-2-A12 curvesSP, SN, ILD, LLS, LLD, MLL, NPHI, RHOB, CAL1, GR, DT, CAL2

You can pass in a list of files or URLs:

p = welly.read_las(['../../tests/assets/P-129_out.LAS',
                    'https://geocomp.s3.amazonaws.com/data/P-130.LAS',
                    'https://geocomp.s3.amazonaws.com/data/R-39.las',
                   ])
0it [00:00, ?it/s]
Only engine='normal' can read wrapped files
1it [00:01,  1.53s/it]
1it [00:01,  1.57s/it]

---------------------------------------------------------------------------
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[4], line 1
----> 1 p = welly.read_las(['../../tests/assets/P-129_out.LAS',
      2                     'https://geocomp.s3.amazonaws.com/data/P-130.LAS',
      3                     'https://geocomp.s3.amazonaws.com/data/R-39.las',
      4                    ])

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/__init__.py:34, in read_las(path, **kwargs)
     20 def read_las(path, **kwargs):
     21     """
     22     A package namespace method to be called as `welly.read_las`.
     23 
   (...)
     32         welly.Project. The Project object.
     33     """
---> 34     return Project.from_las(path, **kwargs)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/welly/project.py:183, in Project.from_las(cls, path, remap, funcs, data, req, alias, max, encoding, printfname, index, **kwargs)
    180 else:
    181     uris = path  # It's a list-like of files and/or URLs.
--> 183 wells = [Well.from_las(f,
    184                        remap=remap,
    185                        funcs=funcs,
    186                        data=data,
    187                        req=req,
    188                        alias=alias,
    189                        encoding=encoding,
    190                        printfname=printfname,
    191                        index=index,
    192                        **kwargs,
    193                        )
    194          for i, f in tqdm(enumerate(uris)) if i < max]
    196 return cls(list(filter(None, wells)))

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

This project has three wells:

p
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
1100/N14A/11E0518 curvesCALI, DT, NPHI_SAN, NPHI_LIM, NPHI_DOL, DPHI_LIM, DPHI_SAN, DPHI_DOL, M2R9, M2R6, M2R3, M2R2, M2R1, GR, SP, PEF, DRHO, RHOB
2303N76434006030022 curvesBS, CALI, CHR1, CHR2, CHRP, CHRS, DRHO, DT1R, DT2, DT2R, DT4P, DT4S, GR, HD1, HD2, HD3, NPOR, PEF, RHOB, SPR1, TENS, VPVS

Typical, the UWIs are a disaster. Let’s ignore this for now.

The Project is really just a list-like thing, so you can index into it to get at a single well. Each well is represented by a welly.Well object.

p[0]
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

Some of the fields of this LAS file are messed up; see the Well notebook for more on how to fix this.

Plot curves from several wells

The DT log is called DT4P in one of the wells. We can deal with this sort of issue with aliases. Let’s set up an alias dictionary, then plot the DT log from each well:

alias = {'Sonic': ['DT', 'DT4P'],
         'Caliper': ['HCAL', 'CALI'],
        }
import matplotlib.pyplot as plt

fig, axs = plt.subplots(figsize=(7, 14),
                        ncols=len(p),
                        sharey=True,
                        )

for i, (ax, w) in enumerate(zip(axs, p)):
    log = w.get_curve('Sonic', alias=alias)
    if log is not None:
        ax = log.plot(ax=ax)
    ax.set_title("Sonic log for\n{}".format(w.uwi))

min_z, max_z = p.basis_range
    
plt.ylim(max_z, min_z)
plt.show()
../_images/d6072e4d08446d2d3c3fd8397ebf58d60c3c6a3b087089977332861461e5fa61.png

Get a pandas.DataFrame

The df() method makes a DataFrame using a dual index of UWI and Depth.

Before we export our wells, let’s give Kennetcook #2 a better UWI:

p[0].uwi = p[0].name
p[0]
Kennetcook #2
Kennetcook #2
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

That’s better.

When creating the DataFrame, you can pass a list of the keys (mnemonics) you want, and use aliases as usual.

alias
{'Sonic': ['DT', 'DT4P'], 'Caliper': ['HCAL', 'CALI']}
keys = ['Caliper', 'GR', 'Sonic']

df = p.df(keys=keys, alias=alias, rename_aliased=True)
df
Caliper GR Sonic
UWI DEPT
Kennetcook #2 1.0668000000 4.3912849426 46.6986503600 NaN
1.2192000000 4.3912849426 46.6986503600 NaN
1.3716000000 4.3912849426 46.6986503600 NaN
1.5240000000 4.3912849426 46.6986503600 NaN
1.6764000000 4.3912849426 46.6986503600 NaN
... ... ... ... ...
303N764340060300 3387.5471999996 303.7090000000 32.0276000039 252.4951
3387.6995999996 303.7090000000 32.0276000000 252.4951
3387.8519999996 303.7090000000 32.0276000000 252.4951
3388.0043999996 303.7090000000 32.0276000000 252.4951
3388.1567999996 303.7090000000 32.0276000000 252.4951

46594 rows × 3 columns

Quality

Welly can run quality tests on the curves in your project. Some of the tests take arguments. You can test for things like this:

  • all_positive: Passes if all the values are greater than zero.

  • all_above(50): Passes if all the values are greater than 50.

  • mean_below(100): Passes if the mean of the log is less than 100.

  • no_nans: Passes if there are no NaNs in the log.

  • no_flat: Passes if there are no sections of well log with the same values (e.g. because a gap was interpolated across with a constant value).

  • no_monotonic: Passes if there are no monotonic ramps in the log (e.g. because a gap was linearly interpolated across).

Insert lists of tests into a dictionary with any of the following key examples:

  • 'GR': The test(s) will run against the GR log.

  • 'Gamma': The test(s) will run against the log matching according to the alias dictionary.

  • 'Each': The test(s) will run against every log in a well.

  • 'All': Some tests take multiple logs as input, for example quality.no_similarities. These test(s) will run against all the logs as a group. Could be quite slow, because there may be a lot of pairwise comparisons to do.

The tests are run against all wells in the project. If you only want to run against a subset of the wells, make a new project for them.

import welly.quality as q

tests = {
    'All': [q.no_similarities],
    'Each': [q.no_gaps, q.no_monotonic, q.no_flat],
    'GR': [q.all_positive],
    'Sonic': [q.all_positive, q.all_between(50, 200)],
}

Let’s add our own test for units:

def has_si_units(curve):
    return curve.units.lower() in ['mm', 'gapi', 'us/m', 'k/m3']

tests['Each'].append(has_si_units)

We’ll use the same alias dictionary as before:

alias
{'Sonic': ['DT', 'DT4P'], 'Caliper': ['HCAL', 'CALI']}

Now we can run the tests and look at the results, which are in an HTML table:

from IPython.display import HTML

HTML(p.curve_table_html(keys=['Caliper', 'GR', 'Sonic', 'SP', 'RHOB'],
                        tests=tests, alias=alias)
    )
IdxUWIDataPassingCaliper*GRSonic*SPRHOB
%3/3 wells3/3 wells3/3 wells2/3 wells3/3 wells
0Kennetcook #25/24 curves54HCAL

4.39 in
GR

78.99 gAPI
DT

63.08 us/ft
SP

52.47 mV
RHOB

2.61 g/cm3
1100/N14A/11E055/18 curves79CALI

8.90 in
GR

103.74 gAPI
DT

74.90 us/ft
SP

101.60 mV
RHOB

2.62 g/cm3
2303N7643400603004/22 curves78CALI

311.97 MM
GR

67.49 GAPI
DT4P

279.84 US/M

RHOB

2493.56 K/M3

Here’s how to interpret the result:

  • Green background: the log is present. You can see the mean value and the units (check them!!).

  • Grey background: the log is not present.

And the traffic light dots (hover to see how many tests passed):

  • Green dot: all the tests passed.

  • Orange dot: some tests failed.

  • Red dot: all tests failed.

  • Grey dot: no tests ran.

The Passing percentage shows how many tests passed for that well.


© 2022 Agile Scientific, CC BY