Visualizing Well Paths With The Welly Python Library

Visualizing Well Paths With The Welly Python Library

Creating 3D Line Plots Using Matplotlib to Visualize Well Trajectories

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.

Video Version of Tutorial

If you prefer to follow along with a video, you can find it below on my YouTube channel.

Python Tutorial

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

Checking the Data

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.

Working with Survey Data and Welly

Importing Survey Data

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.

Adding Survey Data to Welly Well

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.

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])``

Creating Location Plots

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()`.

Add Markers for Start of the Well & End of the Well

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

Compare Against Original Survey

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

Create 3D Plot of Well Path

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.

Creating the 3D Plot

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 widget``
``fig = 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()``

Summary

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.