Title Image

This project is meant to be a fun demonstration of how we can use sensor data from a smart phone in such a way that emulates the real life application of directional drilling in oil & gas wells. Basically, what I want to do is to pretend that I’m drilling a well with my phone in hand, manually controlling the trajectory with the end result being a 3-dimensional plot of the well path along with a set of its surveys. As a drilling engineer, I knew this was possible but had not seen anyone actually do it, so I decided to give it a try.

Sensor Streamer

I found an app called SensorStreamer for my old android phone. It can stream data from the phone’s magnetometers and accelerometers in real time via the local network over UDP in JSON format.

Reading the data from the phone

To read data from the device, I used the python socket module to listen for the data on a port that I specified in the app (8000). In the loop, I used the recv method to read the data. Depending on the buffer size, it sometimes sends in multiple records separated by a new line character \n so I split them out into a list and then filter the list to remove None values. The filter object remains iterable, so I can then load each of the json records from it.

import socket
import json
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#IP Address of phone
host = "192.168.1.4"
port = 8000
buffer_size = 8024
s.connect((host, port))
print("Listening on %s:%s..." % (host, str(port)))

while True:
    data = s.recv(buffer_size)
    mylist = data.decode('utf-8').split('\n')
    mylist = filter(None, mylist) #mylist is now a filter object

    for rec in mylist:
        jdata = json.loads(rec)
        print(jdata)
        time.sleep(1)

Testing this bit of code yields the following output. This is what the JSON records look like. The 3-axis magnetometer and accelerometer readings are included as a nested list in the JSON record.

Listening on 192.168.1.4:8000...
{'accelerometer': {'timestamp': 963062099480427, 'value': [0.08378601, -0.002380371, 9.730026]}, 'magneticField': {'timestamp': 963062117852009, 'value': [-20.129395, -6.564331, -49.739075]}}
{'accelerometer': {'timestamp': 963062290734089, 'value': [0.03590393, -0.07421875, 9.76355]}, 'magneticField': {'timestamp': 963062305352009, 'value': [-21.49353, -5.534363, -50.068665]}}
{'accelerometer': {'timestamp': 963062481987751, 'value': [-0.06463623, 0.007171631, 9.821014]}, 'magneticField': {'timestamp': 963062492852009, 'value': [-20.98236, -7.080078, -49.57428]}}
{'accelerometer': {'timestamp': 963062673271931, 'value': [0.08378601, -0.04069519, 9.662994]}, 'magneticField': {'timestamp': 963062680321492, 'value': [-22.175598, -5.534363, -50.39978]}}
{'accelerometer': {'timestamp': 963062864525593, 'value': [0.08857727, -0.04069519, 9.892838]}, 'magneticField': {'timestamp': 963062867821492, 'value': [-20.300293, -5.3634644, -49.243164]}}
{'accelerometer': {'timestamp': 963063055779255, 'value': [0.06942749, -0.03111267, 9.677353]}, 'magneticField': {'timestamp': 963063055321492, 'value': [-22.175598, -5.8776855, -49.739075]}}

Unpack the JSON to Pandas DataFrame

My first instinct to use the pandas read_json() to read these records resulted in a dataframe with the following structure. I eventually gave up trying to deal with the dataframe in this format.

In [13]: pd.read_json(rec)
Out[13]:
                                         accelerometer                       magneticField
timestamp                             1048967249791200                    1048967333134706
value      [0.04069519, 0.08378601000000001, 9.914383]  [-14.601135, -9.829712, -48.64044]

Rather than try to solve this Rubik’s cube, I essentially just peeled the stickers off and put them in order by directly unpacking the json data into a dictionary and then generated the dataframe from there. See code block below:

def json_rec_to_df(df, mylist):
    for rec in mylist:
        jdata = json.loads(rec)
        df = pd.DataFrame([[jdata['magneticField']['timestamp'], 
                            jdata['magneticField']['value'][0]*1000,
                            jdata['magneticField']['value'][1]*1000,
                            jdata['magneticField']['value'][2]*1000,
                            jdata['accelerometer']['timestamp'],
                            jdata['accelerometer']['value'][0]*(1000/9.80665),
                            jdata['accelerometer']['value'][1]*(1000/9.80665),
                            jdata['accelerometer']['value'][2]*(1000/9.80665)]],
                            columns=['btimestamp', 'bx', 'by', 'bz',
                                     'gtimestamp', 'gx', 'gy', 'gz'])
    return df

This results in a much more user friendly dataframe with a separate column for each data point.

Out[25]:
         btimestamp         bx         by         bz        gtimestamp          gx          gy         gz
0  1048879217758634  37123.108 -34417.725 -17448.425  1048879231277921 -664.550433  559.081032  526.85545

Survey Calculation

To calculate the survey (the 3D orientation) involves a bit of magic. These formulas I picked up along-the-way in my previous career, but I should mention that the minimum curvature calculation is easily found with the google search terms “minimum curvature method”. These are the meat and potatoes of this script. The result is a directional survey in the format MD INC AZI TVD NS EW DLS, which is yielded from the raw 6-axis sensor values bx by bz gx gy gz from the device.

  • MD: The measured depth, or distance in feet between survey stations
  • INC: The inclination of the survey in degrees.
  • AZI: The azimuth of the survey in degrees.
  • TVD: The true vertical depth of the survey in feet.
  • NS: The north/south displacement of the survey.
  • EW: The east/west displacement of the survey.
  • DLS: A combination of the change of inclination and azimuth, expressed in degrees per 100 ft.
  • bx, by, bz: The 3-axis magnetic field components, calculated in nano-Tesla.
  • gx, gy, gz: The 3-axis accelerometer (gravity) components, calculated in milli-G.
  • G: the magnitude of the gravitational vector, calculated in milli-G.
  • B: the total field of the magnetic field vector, also in nano-Tesla.

In the script, I am using the keyboard module to scan for a key press on the space bar such that every time it is pressed, a new survey appears and is added to the list for minimum curvature calculation. Also, since I do not have any drill pipe in my house, I am going to pretend that the survey intervals (MD) are exactly 100 ft.

Example Well Path

Here is an example well path that I “drilled” by holding the phone in the desired orientation. Each dot on the plot represents a “survey” that was recorded when I pressed the space bar. First I started out by holding the phone vertical to keep the “surface hole” nice and straight. Then for the “intermediate” section, I increased the angle of the phone and held it there for a while. Finally, in the “production” section, I increased the angle to horizontal (around 90 degrees) while also making a slight turn here and there.

Example

Final surveys:

In [30]: df_final[['MD', 'INC', 'AZI', 'TVD', 'DLS', 'NS', 'EW']]
Out[30]:
      MD        INC         AZI          TVD        DLS           NS           EW
0    100   2.976223  117.410004          NaN        NaN          NaN          NaN
0    200   2.680754  206.193120   -99.917662   3.962197    -3.294833     1.272906
0    300   5.296719  211.297818  -199.667057   2.637375    -9.338303    -2.157713
0    400   2.328175  209.103517  -299.434648   2.971585   -15.058292    -5.544188
0    500   4.227726  201.187211  -399.266940   1.948257   -20.270382    -7.864539
..   ...        ...         ...          ...        ...          ...          ...
0   7400  89.459584  290.415884 -4773.512982  32.801792  1509.208205 -3076.418735
0   7500  90.369228  316.477383 -4773.665001  26.077065  1563.849897 -3159.140135
0   7600  90.584153  299.284380 -4772.826730  17.193737  1625.019971 -3237.770286
0   7700  90.616705  286.812028 -4771.774646  12.471707  1664.091127 -3329.601194
0   7800  90.413187  288.174885 -4770.875865   1.377913  1694.148895 -3424.970167

[78 rows x 7 columns]

Complete Script

See below for the complete script. Let me know if you try it out! In the future, I might input a real well plan and turn it into a directional drilling video game.

import socket
import json
import time
import pandas as pd
import keyboard
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = "192.168.1.4"
port = 8000
buffer_size = 8024
s.connect((host, port))
print("Listening on %s:%s..." % (host, str(port)))

df = pd.DataFrame(columns = ['btimestamp', 'bx', 'by', 'bz'])

dfsrv = pd.DataFrame()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.set_xlabel('EW (ft)')
ax.set_ylabel('NS (ft)')
ax.set_zlabel('TVD (ft)')

def json_rec_to_df(df, mylist):
    for rec in mylist:
        jdata = json.loads(rec)
        df = pd.DataFrame([[jdata['magneticField']['timestamp'], 
                            jdata['magneticField']['value'][0]*1000,
                            jdata['magneticField']['value'][1]*1000,
                            jdata['magneticField']['value'][2]*1000,
                            jdata['accelerometer']['timestamp'],
                            jdata['accelerometer']['value'][0]*(1000/9.80665),
                            jdata['accelerometer']['value'][1]*(1000/9.80665),
                            jdata['accelerometer']['value'][2]*(1000/9.80665)]],
                            columns=['btimestamp', 'bx', 'by', 'bz',
                                     'gtimestamp', 'gx', 'gy', 'gz'])
    return df

def survey_calc(df):
    df['G'] = (df['gx']**2 + df['gy']**2 + df['gz']**2)**0.5
    df['B'] = (df['bx']**2 + df['by']**2 + df['bz']**2)**0.5
    df['Dip'] = 90 - np.degrees(np.arccos((df.gx*df.bx + df.gy*df.by + df.gz*df.bz) / \
                                          (df.G*df.B)))
    df['INC'] = np.degrees(np.arccos(df.gx / df.G))
    df['INC'] = abs(df['INC'] - 180) #Flip the axis so it lines up with the azimuth 
    df['AZI'] = np.degrees(np.arctan2(df.G*(df.gy*df.bz - df.gz*df.by),
                                      df.bx*df.G**2 - (df.gx*df.bx + df.gy*df.by + \
                                                       df.gz*df.bz)*df.gx)) \
              + np.where((df.gy*df.bz - df.gz*df.by) < 0, 360, 0)
    df['AZI'] = abs(df['AZI'] - 360) #Reverse the values
    return df

def min_curve_calc(MD, I1, I2, A1, A2):
    I1 = np.radians(I1)
    I2 = np.radians(I2)
    A1 = np.radians(A1)
    A2 = np.radians(A2)
    DLS = np.arccos(np.cos(I2 - I1) - (np.sin(I1)*np.sin(I2)*(1-np.cos(A2-A1))))
    RF = (2/DLS) * np.tan(DLS/2)
    NS = (MD / 2) * (np.sin(I1) * np.cos(A1) + np.sin(I2) * np.cos(A2)) * RF
    EW = (MD / 2) * (np.sin(I1) * np.sin(A1) + np.sin(I2) * np.sin(A2)) * RF
    TVD = (MD / 2) * (np.cos(I1) + np.cos(I2)) * RF
    return NS, EW, TVD, DLS, RF

while True:
    data = s.recv(buffer_size)
    mylist = data.decode('utf-8').split('\n')
    mylist = filter(None, mylist) #mylist is now a filter object
    
    df = json_rec_to_df(df, mylist)
    df = survey_calc(df)

    try:
        if keyboard.is_pressed('space'):
            time.sleep(0.5)
            dfsrv = dfsrv.append(df[-1:])
            dfsrv['MD'] = 100
            print(dfsrv[['MD', 'INC', 'AZI', 'gx', 'gy', 'gz', 'bx', 'by', 'bz']][-1:])
            dfsrv['I1'] = dfsrv['INC'].shift(1)
            dfsrv['A1'] = dfsrv['AZI'].shift(1)
            resdf = dfsrv.apply(lambda x: min_curve_calc(x['MD'], 
                                                         x['I1'],
                                                         x['INC'],
                                                         x['A1'],
                                                         x['AZI']), 
                                                         axis=1,
                                                         result_type='expand')
            resdf.columns = ['NS', 'EW', 'TVD', 'DLS', 'RF']
            resdf['TVD'] = -resdf['TVD']  
            df_final = pd.concat([dfsrv, resdf], axis=1)
            df_final['NS'] = df_final['NS'].cumsum() 
            df_final['EW'] = df_final['EW'].cumsum()
            df_final['TVD'] = df_final['TVD'].cumsum()
            df_final['DLS'] = np.degrees(df_final['DLS'])
            df_final['MD'] = df_final['MD'].cumsum()
            ax.scatter(df_final['EW'], df_final['NS'], df_final['TVD'])
            plt.pause(0.5)
    except:
        pass

plt.show()