Visualising Well Paths With the Welly Python Library | Towards Data Science

There are multiple depth references used within well logging to denote a position along the wellbore. These include measured depth (MD), true vertical depth (TVD), true vertical depth subsea (TVDSS) etc. All of which are critical measurements for the successful development and completion of a well.
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.
The majority of LAS files are referenced to measured depth, and often do not contain a TVD curve. As a result, we have to use some maths to calculate the TVD. For this, we require the inclination of the well which is the deviation of the wellbore from vertical, and we also need the azimuth which measures the direction of the well trajectory relative to north.
In this article, we are not going to focus on the calculations behind TVD, instead, we are going to see how we can use the location module of Welly to calculate it. If you want to know more about the maths behind the calculations, I have included a link to a few articles in the description below.
www.drillingformulas.com
If you prefer to follow along with a video, you can find it below on my YouTube channel.
Let's begin with importing the libraries that we will be working with. These are the location and well modules from the welly library and the panda's library.
from welly import Locationfrom welly import Wellimport pandas as pd
Once we have imported these, we can load in the data. The data used in this example comes from the Dutch Sector of the North Sea and can be found on the NLOG website or on my Github Repo.
data = Well.from_las('Netherlands Data/L05-15-Spliced.las')
If we call upon the data variable we have just created, we can get a summary of the well. We can see the well name, the location, and information about the data contained within the file.
If we want a closer look at the data, we can call our well object followed by .data. This provides a dictionary object containing the curve mnemonic and the values contained within the first 3 and last 3 rows. As expected the majority of the data is missing.
data.data{'BHT': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CAL': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CHT': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CN': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CNC': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CNCQH': Curve([nan, nan, nan, ..., nan, nan, nan]), 'CNQH': Curve([nan, nan, nan, ..., nan, nan, nan]), 'GR': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MBVI': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MBVM': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MCBW': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MPHE': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MPHS': Curve([nan, nan, nan, ..., nan, nan, nan]), 'MPRM': Curve([nan, nan, nan, ..., nan, nan, nan]), 'PEQH': Curve([nan, nan, nan, ..., nan, nan, nan]), 'PORZ': Curve([nan, nan, nan, ..., nan, nan, nan]), 'PORZC': Curve([nan, nan, nan, ..., nan, nan, nan]), 'TEN': Curve([nan, nan, nan, ..., nan, nan, nan]), 'TTEN': Curve([nan, nan, nan, ..., nan, nan, nan]), 'WTBH': Curve([   nan,    nan, 87.943, ...,    nan,    nan,    nan]), 'ZCORQH': Curve([nan, nan, nan, ..., nan, nan, nan]), 'ZDEN': Curve([nan, nan, nan, ..., nan, nan, nan]), 'ZDENQH': Curve([nan, nan, nan, ..., nan, nan, nan]), 'ZDNC': Curve([nan, nan, nan, ..., nan, nan, nan]), 'ZDNCQH': Curve([nan, nan, nan, ..., nan, nan, nan])}
A better way to understand the data contents is to look at a well log plot. We can do this by calling upon data.plot and set the keyword argument for extents to curves. We can generate a plot that will start from the first measurement value and go to the last measurement value.
data.plot(extents='curves')
This returns the following plot. This plot allows us to quickly see the contents of each logging measurement and their character, it can be difficult to see if you have a large number of curves present in your data.
We won’t be going into the plotting of the log data in this article, but if you are interested, be sure to check out my YouTube video in the welly series where I focus on working with single and multiple wells.
Now that we have the log data loaded, we can begin to load the survey data.
Survey data is commonly measured at irregular intervals during the drilling process. It gives a snapshot of the measured depth (MD), inclination (INC) and azimuth (AZI) at the time of the survey. From this, we can calculate the True Vertical Depth (TVD), x-offset and y-offset from the origin point of the well.
Commonly the data is provided in a table or CSV format. To load the CSV data we can use pd.read_csv() and pass in the location of the file and its name.
survey = pd.read_csv('Netherlands Data/L05-15-Survey.csv')
When we call upon the survey object, we see we have a dataframe returned containing the key well position information.
Welly requires the survey data to contain measured depth, inclination, and azimuth. This allows the library to calculate the TVD, X-offset and Y-offset.
We can subset the data by calling upon the survey dataframe and using square brackets to pass in a list of the desired column names.
survey_subset = survey[['MD', 'INC', 'AZI']]
When we call upon survey_subset, we get back the following dataframe.
After the survey data has been loaded from a CSV file, we need to calculate our location parameters. By default, Welly is set to calculate these using the minimum curvature method, which is the most common and most accurate method for this purpose.
More information on this method can be found at https://petrowiki.spe.org/Calculation_methods_for_directional_survey
To add the survey data to the well we can call upon the following
#Add deviation data to a welldata.location.add_deviation(survey_subset.values)
Now that the survey data has been loaded into our Welly well object, we can call upon data.location.position to view the data. For this example, I have used slicing notation to return just the first 5 rows of the data.
#View first five rows of the datadata.location.position[:5]
The returned array is formatted as: X-offset, Y-offset, and TVD.
array([[  0.        ,   0.        ,   0.        ],       [  0.        ,   0.        ,  89.3       ],       [ -0.6333253 ,   0.8552655 , 142.08569704],       [ -1.59422229,   2.03112298, 170.14372544],       [ -3.19869524,   3.75201703, 197.74222054]])
We can extract each of the location parameters into variables by slicing up the array. This is done using square brackets and selecting all rows using the colon (:) followed by the column within the array.
x_loc = data.location.position[:,0]y_loc = data.location.position[:,1]z_loc = data.location.position[:,2]
If we call upon z_loc, which is our TVD, we get back the following array:
array([   0.        ,   89.3       ,  142.08569704,  170.14372544,        197.74222054,  225.68858529,  254.17872844,  282.83986178,        311.3294853 ,  339.82739229,  368.42706739,  396.92691062,        425.62638313,  454.22551155,  482.42473573,  511.12342097,        539.72042719,  568.21483874,  597.00539705,  625.8900492 ,        654.36614119,  683.22656973,  711.6691264 ,  740.00649462,        767.54748074,  797.06893638,  825.36408467,  853.83548556,        882.30553194,  910.5784206 ,  939.03148052,  967.35658945,        995.56380403, 1023.95695144, 1052.22740711, 1080.54668678,       1108.68959153, 1136.6589388 , 1164.87003188, 1192.91335907,       1220.78632672, 1248.71483434, 1276.69724251, 1304.38501765,       1332.02759325, 1359.48829109, 1386.59399864, 1413.47807554,       1440.51055639, 1467.37758752, 1494.27990524, 1521.15255355,       1547.94826077, 1574.81148851, 1601.67556214, 1628.46190115,       1655.38744119, 1682.77094459, 1709.94467279, 1737.02953371,       1764.09191195, 1791.3868565 , 1818.75450935, 1845.99897829,       1873.48895669, 1900.86728951, 1928.20315443, 1955.1719983 ,       1982.16522007, 2009.02433087, 2035.75920778, 2062.44460278,       2088.89113734, 2115.18715337, 2141.53399746, 2167.86835015,       2194.17601217, 2220.34087524, 2246.65950847, 2273.26101123,       2300.13882036, 2326.97261339, 2353.95042418, 2380.81977995,       2407.70173751, 2434.4676547 , 2460.90920154, 2472.20902514,       2498.66491934, 2525.74629926, 2553.35452297, 2579.86481719,       2606.67927736, 2634.67341768, 2663.73057678, 2690.48389425,       2716.3110963 , 2743.39559139, 2770.53319932, 2798.10117685,       2824.99473242, 2851.85337513, 2879.55133503, 2906.56976579,       2933.96384651, 2960.25680057, 2986.50202763, 3013.35506117,       3039.2427437 , 3065.81112303, 3078.05551274, 3096.92997476])
We can also access the same data by calling upon data.location.tvd.
array([   0.        ,   89.3       ,  142.08569704,  170.14372544,        197.74222054,  225.68858529,  254.17872844,  282.83986178,        311.3294853 ,  339.82739229,  368.42706739,  396.92691062,        425.62638313,  454.22551155,  482.42473573,  511.12342097,        539.72042719,  568.21483874,  597.00539705,  625.8900492 ,        654.36614119,  683.22656973,  711.6691264 ,  740.00649462,        767.54748074,  797.06893638,  825.36408467,  853.83548556,        882.30553194,  910.5784206 ,  939.03148052,  967.35658945,        995.56380403, 1023.95695144, 1052.22740711, 1080.54668678,       1108.68959153, 1136.6589388 , 1164.87003188, 1192.91335907,       1220.78632672, 1248.71483434, 1276.69724251, 1304.38501765,       1332.02759325, 1359.48829109, 1386.59399864, 1413.47807554,       1440.51055639, 1467.37758752, 1494.27990524, 1521.15255355,       1547.94826077, 1574.81148851, 1601.67556214, 1628.46190115,       1655.38744119, 1682.77094459, 1709.94467279, 1737.02953371,       1764.09191195, 1791.3868565 , 1818.75450935, 1845.99897829,       1873.48895669, 1900.86728951, 1928.20315443, 1955.1719983 ,       1982.16522007, 2009.02433087, 2035.75920778, 2062.44460278,       2088.89113734, 2115.18715337, 2141.53399746, 2167.86835015,       2194.17601217, 2220.34087524, 2246.65950847, 2273.26101123,       2300.13882036, 2326.97261339, 2353.95042418, 2380.81977995,       2407.70173751, 2434.4676547 , 2460.90920154, 2472.20902514,       2498.66491934, 2525.74629926, 2553.35452297, 2579.86481719,       2606.67927736, 2634.67341768, 2663.73057678, 2690.48389425,       2716.3110963 , 2743.39559139, 2770.53319932, 2798.10117685,       2824.99473242, 2851.85337513, 2879.55133503, 2906.56976579,       2933.96384651, 2960.25680057, 2986.50202763, 3013.35506117,       3039.2427437 , 3065.81112303, 3078.05551274, 3096.92997476])
To understand the position of the well, we can call upon three plots.
X-offset vs Y-offset (Topographical View)
X-offset vs TVD
Y-offset vs TVD
To create these plots we can use matplotlib and create multiple plots using subplot2grid.
import matplotlib.pyplot as pltfig, ax = plt.subplots(figsize=(15,5))ax1 = plt.subplot2grid(shape=(1,3), loc=(0,0))ax2 = plt.subplot2grid(shape=(1,3), loc=(0,1))ax3 = plt.subplot2grid(shape=(1,3), loc=(0,2))ax1.plot(x_loc, y_loc, lw=3)ax1.set_title('X Location vs Y Location')ax2.plot(x_loc, z_loc, lw=3)ax2.set_title('X Location vs TVD')ax3.plot(y_loc, z_loc, lw=3)ax3.set_title('Y Location vs TVD')Text(0.5, 1.0, 'Y Location vs TVD')
This returns our three well profile plots.
Notice that the two TVD plots are reversed, we will sort this using ax.invert_yaxis().
We can add markers to our plot to show the starting location (black square) and the end location (red star) of the well. Additionally, we will invert the y-axis of the two TVD plots.
fig, ax = plt.subplots(figsize=(15,5))ax1 = plt.subplot2grid(shape=(1,3), loc=(0,0))ax2 = plt.subplot2grid(shape=(1,3), loc=(0,1))ax3 = plt.subplot2grid(shape=(1,3), loc=(0,2))ax1.plot(x_loc, y_loc, lw=3)ax1.plot(x_loc[0], y_loc[0], marker='s', color='black', ms=8)ax1.plot(x_loc[-1], y_loc[-1], marker='*', color='red', ms=8)ax1.set_title('X Location vs Y Location')ax2.plot(x_loc, z_loc, lw=3)ax2.plot(x_loc[0], z_loc[0], marker='s', color='black', ms=8)ax2.plot(x_loc[-1], z_loc[-1], marker='*', color='red', ms=8)ax2.invert_yaxis()ax2.set_title('X Location vs TVD')ax3.plot(y_loc, z_loc, lw=3)ax3.plot(y_loc[0], z_loc[0], marker='s', color='black', ms=8)ax3.plot(y_loc[-1], z_loc[-1], marker='*', color='red', ms=8)ax3.invert_yaxis()ax3.set_title('Y Location vs TVD')Text(0.5, 1.0, 'Y Location vs TVD')
We are fortunate enough to have a survey file that contains the location parameters and TVD — this may not always be the case. We can do a quick check with what Welly has calculated and the ones contained within the CSV file by adding that data to our plots.
fig, ax = plt.subplots(figsize=(15,5))ax1 = plt.subplot2grid(shape=(1,3), loc=(0,0))ax2 = plt.subplot2grid(shape=(1,3), loc=(0,1))ax3 = plt.subplot2grid(shape=(1,3), loc=(0,2))ax1.plot(x_loc, y_loc, lw=7)ax1.plot(x_loc[0], y_loc[0], marker='s', color='black', ms=8)ax1.plot(survey['X-offset'], survey['Y-offset'])ax1.plot(x_loc[-1], y_loc[-1], marker='*', color='red', ms=8)ax1.set_title('X Location vs Y Location')ax2.plot(x_loc, z_loc, lw=7)ax2.plot(x_loc[0], z_loc[0], marker='s', color='black', ms=8)ax2.plot(survey['X-offset'], survey['TVD'])ax2.plot(x_loc[-1], z_loc[-1], marker='*', color='red', ms=8)ax2.invert_yaxis()ax2.set_title('X Location vs TVD')ax3.plot(y_loc, z_loc, lw=7)ax3.plot(y_loc[0], z_loc[0], marker='s', color='black', ms=8)ax3.plot(survey['Y-offset'], survey['TVD'])ax3.plot(y_loc[-1], z_loc[-1], marker='*', color='red', ms=8)ax3.invert_yaxis()ax3.set_title('X Location vs TVD')Text(0.5, 1.0, 'X Location vs TVD')
Rather than viewing the data in two dimensions, we can view it in three dimensions using matplotlib. But first, we have to calculate continuous data. This is done by using location.trajectory(). Here we can provide a datum, i.e the UTM co-ordinates of the well location at the surface, and a vertical offset.
If we look at the following image from the NLOG website, we have the exact surface co-ordinates of the well
We will use the Delivered Location co-ordinates in the location.trajectory() function.
# Create a trajectory of regularly sampled pointslocation_data = data.location.trajectory(datum=[589075.56, 5963534.91, 0], elev=False)xs = location_data[:,0]ys = location_data[:,1]zs = location_data[:,2]plt.plot(xs, ys)plt.xlabel('X Location')plt.ylabel('Y Location')plt.ticklabel_format(style='plain')plt.grid()
When we run this code, we now have a topographical view of our well and the values reflect the true co-ordinates of the well.
Now that the well location has been referenced to a datum, we can move onto plotting our well path. The previous steps do not need to be applied and you can view this using the x, y and z location
To create the 3D plot, we need to import Axes3D from mpl_toolkits.mplot3d and then when we need to enable 3D plotting by using a magic Jupyter command: %matplotlib widget .
We then create the figure and set the projection to 3d as seen in the following code.
from mpl_toolkits.mplot3d import Axes3D# Enable 3D Ploting%matplotlib widgetfig = plt.figure(figsize=(8, 8))ax = plt.axes(projection='3d')ax.plot3D(xs, ys, zs, lw=10)ax.set_zlim(3000, 0)ax.set_xlabel('X Location')ax.set_ylabel('Y Location')ax.set_zlabel('TVD')plt.ticklabel_format(style='plain')plt.show()
Within this short tutorial, we have seen how to combine raw well survey data with a Welly well object and visualise the well path in plan and side views. We have also seen how we can visualise the well path in 3D using matplotlib. In doing this, we can get a better understanding of the well path trajectory.
Thanks for reading!
If you have found this article useful, please feel free to check out my other articles looking at various aspects of Python and well log data. You can also find my code used in this article and others at GitHub.
If you want to get in touch you can find me on LinkedIn or at my website.
Interested in learning more about python and well log data or petrophysics? Follow me on Medium.
If you enjoy reading these tutorials and want to support me as a writer and creator, then please consider signing up to become a Medium member. It’s $5 a month and you get unlimited access to many thousands of articles on a wide range of topics. If you sign up using my link, I will earn a small commission with no extra cost to you!