The world’s leading publication for data science, AI, and ML professionals.

Working with Well Trajectories in Python

Convert raw well survey data to complete positional data with wellpathpy

Photo by James Wheeler: https://www.pexels.com/photo/photo-of-pathway-surrounded-by-fir-trees-1578750/
Photo by James Wheeler: https://www.pexels.com/photo/photo-of-pathway-surrounded-by-fir-trees-1578750/

Depth is an essential measurement when working with subsurface data. It is used to tie multiple sets of data to a single reference. There are numerous depth references used to identify a position within the subsurface. These include Measured Depth (MD), True Vertical Depth (TVD), and True Vertical Depth Subsea (TVDSS).

Illustration of key references and depth names within well drilling & logging. Image by the author.
Illustration of key references and depth names within well drilling & logging. Image by the author.

The above image illustrates the key depth references that are referred to within this article.

  • Measured Depth (MD) is the length of the wellbore measured along its length.
  • True Vertical Depth (TVD), is the absolute vertical distance between a datum, such as the rotary table, and a point in the wellbore.
  • True Vertical Depth Sub Sea (TVDSS), is the absolute vertical distance between mean sea level and a point in the wellbore.

When a well is vertical, MD is equal to TVD when measured from the same datum. In the case where the well is deviated, the TVD value becomes less than the MD value.

When wells are drilled, survey measurements are often taken to ensure that the well is going in the intended direction. This generates a dataset with sparse measurements containing the wellbore’s inclination (how deviated the wellbore is from vertical), azimuth relative to north, and the current measured depth.

To convert this to regularly sampled data, similar to the sample rates used within well log measurements we need to apply trigonometric calculations

Within this tutorial, we will see how we can use a Python library called wellpathpy to generate complete positional logs.

Video Tutorial

A video version of this tutorial is available on my YouTube channel if you want to see this tutorial in action.

wellpathpy

wellpathpy is a Python library that has been developed to take well survey data and calculate positional logs from a variety of industry-standard methods. This allows us to generate TVD, TVDSS, Northings and Eastings.

You can find out more about the library in the GitHub repo linked below.

GitHub – Zabamund/wellpathpy: Well deviation import

If you do not already have this library installed, you can use pip to install it like so:

pip install wellpathpy

Data Used Within This Tutorial

The data used within this tutorial is a subset of the Volve Dataset that was released by Equinor in 2018. Full details of the dataset, including the licence, can be found at the link below.

Volve field data set

The Volve data license is based on CC BY 4.0 license. Full details of the license agreement can be found here:

https://cdn.sanity.io/files/h61q9gi9/global/de6532f6134b9a953f6c41bac47a0c055a3712d3.pdf?equinor-hrs-terms-and-conditions-for-licence-to-data-volve.pdf

Importing Libraries & Data

The first step in our tutorial is to import the libraries we will work with.

In this case, we will be working primarily with 3 libraries: wellpathpy, matplotlib for visualising our well paths and pandas for creating our final dataframe.

We are also importing numpy, which is used to suppress scientific notation within the arrays generated by wellpathpy. This is achieved by using the np.set_printoptions() function and passing in supress=True.

import wellpathpy as wp
import Matplotlib.pyplot as plt
import pandas as pd
import numpy as np
np.set_printoptions(suppress=True)

Once the libraries have been imported, the next step is to import the survey data. The raw csv file contains three columns that have been named md, inc and azi.

Once we have our csv file in this format we can assign three variables: md, inc and azito the function call wp.read_csv().

md, inc, azi = wp.read_csv('Data/Volve/15_9-F-12_Survey_Data.csv')

There are a number of checks performed behind the scenes to make sure that the data is valid. These include:

  • Measured Depth (md) increases monotonically
  • Columns are in the correct order of md, inc and azi
  • Inclination (inc) contains values between 0 and 180 degrees
  • Azimuth (azi) contains values between 0 and 360 degrees

Once the data has been loaded, we can call upon md and view the data. this allows us to check if the data has been loaded in successfully.

Example of measured depth data after being loaded into wellpathpy. Image by the author.
Example of measured depth data after being loaded into wellpathpy. Image by the author.

Loading Survey Data to wellpathpy

After loading the data we now need to create a wellpathpy deviation object. This is initiated as follows:

dev = wp.deviation(md, inc, azi)
dev

When we view the dev object we see that we have the md, inc and azi data stored within arrays.

We can access each of these by calling upon them like so:

dev.md
dev.inc
dev.azi

Visualising the Raw Survey Data

We can visualise this raw data very simply in matplotlib by creating a figure containing two subplots. One for inclination (inc) and one for azimuth (azi). This will allow us to visualise how the well trajectory varies with measured depth.

fig, ax = plt.subplots(1, 2, figsize=(8,10))
ax1 = plt.subplot2grid((1,2), (0,0))
ax2 = plt.subplot2grid((1,2), (0,1))
ax1.plot(dev.inc, dev.md, color = "black", marker='.', linewidth=0)
ax1.set_ylim(dev.md[-1], 0)
ax1.set_xlim(0, 90)
ax1.set_xlabel('Deviation', fontweight='bold', fontsize=14)
ax1.set_ylabel('Measured Depth', fontweight='bold', fontsize=14)
ax1.grid(color='lightgrey')
ax1.set_axisbelow(True)
ax2.plot(dev.azi, dev.md, color = "black", marker='.', linewidth=0)
ax2.set_ylim(dev.md[-1], 0)
ax2.set_xlim(0, 360)
ax2.set_xlabel('Azimuth', fontweight='bold', fontsize=14)
ax2.grid(color='lightgrey')
ax2.set_axisbelow(True)
plt.show()
Wellbore azimuth and inclination from raw survey measurements. Image by the author.
Wellbore azimuth and inclination from raw survey measurements. Image by the author.

In the plot above we can see:

  • The borehole starts off vertical until we get to around 500 m, before gradually increasing and then decreasing. Eventually, the wellbore is landed within the target interval at an inclination of 53.43 degrees.
  • As the borehole starts off vertical, our azimuth is set to due North (0 degrees), before heading to the North East and then turning back towards the South, and ending up in an easterly direction

Later on within this article, we will be able to visualise this data in 3D and it will make much more sense.

Create Positional Logs From Survey Data

Survey data is sporadically sampled with large gaps in between measurements. We can resample our data so that we have a measurement every 1 m.

To do this we first define what our depth step will be, and then we create a list of evenly spaced values between 0 and the last md value.

depth_step = 1
depths = list(range(0, int(dev.md[-1]) + 1, depth_step))

wellpathpy contains multiple methods for calculating True Vertical Depth (TVD), however, we will focus on the Minimum Curvature Method. Check out the link below if you want to understand the maths behind this methodology.

Minimum Curvature Method

To create our TVD curve and positional measurements (Northing and Easting) we need to call upon the following code. This will also resample our depth values so that we have a measurement every 1 m.

pos = dev.minimum_curvature().resample(depths = depths)

When we call upon pos we get back a position object with three arrays like this:

Creating Resampled Survey Data

To update our original survey data to the same depth step of 1 m, we can resample that data by calling upon pos.deviation()

resampled_dev = pos.deviation()

And when we view resampled_dev we get back the following array.

As with any calculation where we are resampling data, it is good practice to compare the results with the original.

The code below is the same as the plot created above, but now contains the resampled data from the variable resampled.

fig, ax = plt.subplots(1, 2, figsize=(8,10))
ax1 = plt.subplot2grid((1,2), (0,0))
ax2 = plt.subplot2grid((1,2), (0,1))
ax1.plot(dev.inc, dev.md, color = "black", marker='.', linewidth=0)
ax1.plot(resampled_dev.inc, resampled_dev.md, color='red')
ax1.set_ylim(dev.md[-1], 0)
ax1.set_xlim(0, 90)
ax1.set_xlabel('Deviation', fontweight='bold', fontsize=14)
ax1.set_ylabel('Measured Depth', fontweight='bold', fontsize=14)
ax1.grid(color='lightgrey')
ax1.set_axisbelow(True)
ax2.plot(dev.azi, dev.md, color = "black", marker='.', linewidth=0)
ax2.plot(resampled_dev.azi,resampled_dev.md, color='red')
ax2.set_ylim(dev.md[-1], 0)
ax2.set_xlim(0, 360)
ax2.set_xlabel('Azimuth', fontweight='bold', fontsize=14)
ax2.grid(color='lightgrey')
ax2.set_axisbelow(True)
plt.show()

When the plot appears, we can see the original data as black points, and the resampled data as a red line. Overall this looks good, and the values appear to be in agreement with each other.

Wellbore azimuth and inclination from raw survey measurements and resampled data from wellpathpy. Image by the author.
Wellbore azimuth and inclination from raw survey measurements and resampled data from wellpathpy. Image by the author.

Creating Positional Plots

Now that we have resampled the data and created positional logs, we can visualise our positional data with the following code. This allows us to generate three plots:

  • North/South position vs East/West position (topographical view)
  • Variation in East/West position with depth
  • Variation in North/South position with depth
fig, ax = plt.subplots(2, 2, figsize=(15,5))
ax1 = plt.subplot2grid((1,3), (0,0))
ax2 = plt.subplot2grid((1,3), (0,1))
ax3 = plt.subplot2grid((1,3), (0,2))
ax1.plot(pos.easting, pos.northing, color = "black", linewidth=2)
ax1.set_xlim(-500, 400)
ax1.set_ylim(-400, 100)
ax1.set_xlabel('West (-) / East (+)', fontweight='bold', fontsize=14)
ax1.set_ylabel('South (-) / North (+)', fontweight='bold', fontsize=14)
ax1.grid(color='lightgrey')
ax1.set_axisbelow(True)
ax2.plot(pos.easting, pos.depth, color = "black", linewidth=2)
ax2.set_xlim(-500, 400)
ax2.set_ylim(3500, 0)
ax2.set_xlabel('West (-) / East (+)', fontweight='bold', fontsize=14)
ax2.set_ylabel('Depth', fontweight='bold', fontsize=14)
ax2.grid(color='lightgrey')
ax2.set_axisbelow(True)
ax3.plot(pos.northing, pos.depth, color = "black", linewidth=2)
ax3.set_xlim(-500, 400)
ax3.set_ylim(3500, 0)
ax3.set_xlabel('South (-) / North (+)', fontweight='bold', fontsize=14)
ax3.set_ylabel('Depth', fontweight='bold', fontsize=14)
ax3.grid(color='lightgrey')
ax3.set_axisbelow(True)
plt.tight_layout()
plt.show()

This generates the following plot.

Wellbore position from different perspectives to better understand the well trajectory. Image by the author.
Wellbore position from different perspectives to better understand the well trajectory. Image by the author.

Calculating TVDSS

One of the common outputs from these calculations is TVDSS. This provides us with the absolute vertical difference between a datum, such as the rotary table or drill floor and a position within the wellbore.

When it is calculated we have positive values above mean sea level, and negative values below mean sea level.

Illustration of key references and depth names within well drilling & logging. Image by the author.
Illustration of key references and depth names within well drilling & logging. Image by the author.

To calculate TVDSS, we simply call upon pos.to_tvdss() and pass in our permanent datum.


pos_tvdss = pos.to_tvdss(datum_elevation=30)

When run, it returns the following position object with multiple arrays. The first one labelled depth represents our TVDSS data.

Building a 3D Well Path Plot with Plotly

Looking at well position on 2D plots can be very limiting, especially if you are trying to understand that wellbore’s position and shape. Within this section, we will see how can create an interactive 3D plot in Python and

But, before we generate the plot, we can optionally position our wellbore in the right place.

Setting Up the Reference Location

The current northing and easting for our position object starts from 0. We can correct that location by providing a surface location for the rig.

wellhead_northing = 6478572
wellhead_easting = 435050
pos_wellhead = pos.to_wellhead(surface_northing=wellhead_northing,
                               surface_easting=wellhead_easting)

After running this section of code we can check our northing for our wellbore position to see that the datum has been updated.

pos_wellhead.northing

Creating the 3D Well Path Plot

Now that the wellbore has been referenced to the right location, we can now make our 3D plot.

For this, we are going to rely on plotly rather than matplotlib. In my view, it generates a much better interactive plot compared to matplotlib.

The code below is used to generate the 3D plot. If you do not have plotly installed, you can do so within Jupyter Notebooks using !pip install plotly.

import plotly
import plotly.graph_objs as go
# Configure Plotly to be rendered within the notebook
plotly.offline.init_notebook_mode()
# Configure the trace.
wellpath = go.Scatter3d(
    x=pos_wellhead.easting,  
    y=pos_wellhead.northing,
    z=pos_wellhead.depth,
    mode='markers',
    marker={
        'size': 5,
        'opacity': 0.8,
    }
)
data = [wellpath]
fig = go.Figure(data=data)
fig.update_layout(scene = dict(
                    zaxis_autorange="reversed",
                    xaxis_title='West (-) / East (+) (m)',
                    yaxis_title='South (-) / North (+) (m)',
                    zaxis_title='TVD (m)'),
                    width=800,
                    margin=dict(r=20, b=10, l=10, t=10))
plotly.offline.iplot(fig)

When the above code is executed, we generate the following plot which can be moved around. Note that to show the z-axis in terms of increasing depth as we go down, we need to reverse the z-axis by updating the layout with zaxis_autorange="reversed" .

Interactive 3D well path trajectory created with plotly. Image by the author.
Interactive 3D well path trajectory created with plotly. Image by the author.

Creating a Pandas DataFrame of Resampled Survey Data and Positional Data

When working with well log data in Python, it is common to work with dataframes. wellpathpy does not provide a direct export to this format, but we can easily create a dataframe like so:

#Create a dictionary of the curve names and the data
data = {'MD':resampled_dev.md, 
        'AZI':resampled_dev.azi,
        'INC':resampled_dev.inc,
        'TVD':pos.depth,
        'TVDSS':pos_tvdss.depth,
        'YLOC':pos.northing,
        'XLOC':pos.easting,
        'NORTHING': pos_wellhead.northing,
        'EASTING': pos_wellhead.easting}
df = pd.DataFrame(data)

This will return the following dataframe with all of our calculated positional data and survey measurements.

Combined well survey and positional data within a pandas dataframe. Image by the author.
Combined well survey and positional data within a pandas dataframe. Image by the author.

Summary

Converting raw well survey data to regularly sampled positional measurements is a simple process with the wellpathpy library. Once that data has been generated, we can create interactive plots to better visualise and understand the wellbore trajectory. Additionally, this data can now be integrated with well log data from the same well in order to have a complete composite well dataset.


Thanks for reading. Before you go, you should definitely subscribe to my content and get my articles in your inbox. You can do that here! Alternatively, you can sign up for my newsletter to get additional content straight into your inbox for free.

Secondly, you can get the full Medium experience and support me and thousands of other writers by signing up for a membership. It only costs you $5 a month, and you have full access to all of the amazing Medium articles, as well as the chance to make money with your writing.

If you sign up using my link, you will support me directly with a portion of your fee, and it won’t cost you more. If you do so, thank you so much for your support!


Related Articles