Update well tops from NPD#

In this tutorial we’ll update the Groups from the Petrel well tops folder using the data published on the Norwegian Petroleum Directorate (NPD).

All data used in this notebook comes from the FORCE wells dataset:

NPD, 2021, FORCE 2020 Lithology Machine Learning Competition Results: https://www.npd.no/en/force/Previous-events/results-of-the-FORCE-2020-lithology-competition/

Import packages#

[2]:
from cegalprizm.pythontool import PetrelConnection
import pandas as pd
import numpy as np

Connect to Petrel#

[2]:
ptp = PetrelConnection(allow_experimental=True)
print('Connected to {}'.format(ptp.get_current_project_name()))
Connected to WellTopsDemo.pet

Import NPD welltops#

We start by accessing the NPD url pointing to the excel table which contains the welltop information and assgin that to a dataframe. Then we format the dataframe by drop some unnecessary columns, renaming some other columns and droping the information regarding Formations:

[12]:
NPD_WT_df=pd.read_excel("https://factpages.npd.no/ReportServer_npdpublic?/FactPages/tableview/strat_litho_wellbore&rs:Command=Render&rc:Toolbar=false&rc:Parameters=f&IpAddress=not_used&CultureCode=en&rs:Format=EXCEL&Top100=false",sheet_name='strat_litho_wellbore')
NPD_WT_df=NPD_WT_df.drop(columns=['NPDID lithostrat. unit','Wellbore completion date','NPDID wellbore','Date updated','Level','Date sync NPD','Bottom depth [m]'])
NPD_WT_GP_df= NPD_WT_df[NPD_WT_df['Lithostrat. unit'].str.contains(' GP')]
NPD_WT_GP_df.rename(columns = {'Lithostrat. unit':'Surface','Top depth [m]':'MD','Wellbore name':'Well'}, inplace = True)

NPD_WT_GP_df['Surface']=NPD_WT_GP_df['Surface'].str.replace('GP', 'GP. Top')

NPD_WT_GP_df
C:\Users\vladro\AppData\Local\Temp\ipykernel_27244\1295568092.py:4: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  NPD_WT_GP_df.rename(columns = {'Lithostrat. unit':'Surface','Top depth [m]':'MD','Wellbore name':'Well'}, inplace = True)
C:\Users\vladro\AppData\Local\Temp\ipykernel_27244\1295568092.py:6: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  NPD_WT_GP_df['Surface']=NPD_WT_GP_df['Surface'].str.replace('GP', 'GP. Top')
[12]:
Well MD Surface
0 1/2-1 94.0 NORDLAND GP. Top
1 1/2-1 1777.0 HORDALAND GP. Top
3 1/2-1 3059.0 ROGALAND GP. Top
7 1/2-1 3407.0 SHETLAND GP. Top
10 1/2-2 114.0 NORDLAND GP. Top
... ... ... ...
38612 7335/3-1 1162.0 SASSENDALEN GP. Top
38616 7435/12-1 285.0 NORDLAND GP. Top
38618 7435/12-1 291.0 ADVENTDALEN GP. Top
38623 7435/12-1 576.5 KAPP TOSCANA GP. Top
38629 7435/12-1 1168.0 SASSENDALEN GP. Top

12082 rows × 3 columns

Create the Petrel welltop DB#

Next, we generate a dataframe from the marker collection located in our Petrel project. We drop the information regarding the Formations and we rename some columns:

[4]:
# Get the markercollection in a data frame
mc = ptp.markercollections['Input/Groups']
df_mc = mc.as_dataframe(False)

#make a new dataframe using just 3 attributes from the marker collection
newmc_df=df_mc[['Well identifier (Well name)','MD','Surface']]

#Only keep the Groups from the marker collection
Gp_df=newmc_df[newmc_df['Surface'].str.contains('GP.')]
Gp_df=Gp_df.drop_duplicates()

#Sort the dataframe and rename a column
Gp_df=Gp_df.sort_values(by=[ 'Well identifier (Well name)'], ascending=[True])
Gp_df.rename(columns = {'Well identifier (Well name)':'Well'}, inplace = True)
Gp_df
[4]:
Well MD Surface
53 15/9-14 2756.0 SHETLAND GP. Top
19 15/9-14 2387.0 ROGALAND GP. Top
83 15/9-14 3082.0 CROMER KNOLL GP. Top
5 15/9-14 3176.0 VIKING GP. Top
75 15/9-14 1078.0 HORDALAND GP. Top
... ... ... ...
48 35/9-8 392.0 NORDLAND GP. Top
89 35/9-8 2705.0 CROMER KNOLL GP. Top
27 35/9-8 1221.0 ROGALAND GP. Top
11 35/9-8 2714.0 VIKING GP. Top
60 35/9-8 1738.0 SHETLAND GP. Top

112 rows × 3 columns

Format the NPD welltop DB#

Finally, we eliminate any well which does not exist in the Petrel project from the NPD dataframe and we sort the two data frames on both the well name and the measured depth:

[5]:
NPD_cut=pd.DataFrame()
well_list = list(Gp_df.Well.unique())
for l in range(len(well_list)):
    NPD_cut=pd.concat([NPD_cut,NPD_WT_GP_df[NPD_WT_GP_df['Well'].str.fullmatch(well_list[l])]],axis=0, ignore_index=True)
NPD_cut.sort_values(["Well","MD"], axis = 0, ascending=True, inplace=True,na_position="first")
Gp_df.sort_values(["Well","MD"], axis = 0, ascending=True, inplace=True,na_position="first")
print(NPD_cut,'\n',Gp_df)
        Well      MD               Surface
0    15/9-14   128.0      NORDLAND GP. Top
1    15/9-14  1078.0     HORDALAND GP. Top
2    15/9-14  2387.0      ROGALAND GP. Top
3    15/9-14  2756.0      SHETLAND GP. Top
4    15/9-14  3082.0  CROMER KNOLL GP. Top
..       ...     ...                   ...
148   35/9-8  1221.0      ROGALAND GP. Top
149   35/9-8  1737.5      SHETLAND GP. Top
150   35/9-8  2704.5  CROMER KNOLL GP. Top
151   35/9-8  2714.0        VIKING GP. Top
152   35/9-8  3146.0         BRENT GP. Top

[153 rows x 3 columns]
        Well      MD               Surface
36  15/9-14   128.0      NORDLAND GP. Top
75  15/9-14  1078.0     HORDALAND GP. Top
19  15/9-14  2387.0      ROGALAND GP. Top
53  15/9-14  2756.0      SHETLAND GP. Top
83  15/9-14  3082.0  CROMER KNOLL GP. Top
..      ...     ...                   ...
27   35/9-8  1221.0      ROGALAND GP. Top
60   35/9-8  1738.0      SHETLAND GP. Top
89   35/9-8  2705.0  CROMER KNOLL GP. Top
11   35/9-8  2714.0        VIKING GP. Top
64   35/9-8  3146.0         BRENT GP. Top

[112 rows x 3 columns]

Check which well tops are missing#

Now we’ll compare the two dataframes and for each well, we identify the Group inforamtion missing from the Petrel project:

[6]:
for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['Surface'], keep=False)
    if len(missing) == 0:
        print(well_list[l],'  is up to date' )
    elif len(missing) == 1:
        print(well_list[l], 'is missing the  ', missing['Surface'].values[0])
    else:
        for i in range(len(missing['Surface'])):
            print(well_list[l], 'is missing the  ', missing['Surface'].values[i])
15/9-14   is up to date
15/9-23   is up to date
16/2-7 is missing the   NORDLAND GP. Top
16/2-7 is missing the   HORDALAND GP. Top
16/2-7 is missing the   ROGALAND GP. Top
16/2-7 is missing the   SHETLAND GP. Top
16/2-7 is missing the   CROMER KNOLL GP. Top
16/2-7 is missing the   VIKING GP. Top
16/2-7 is missing the   VESTLAND GP. Top
16/2-7 is missing the   HEGRE GP. Top
16/2-7 is missing the   ZECHSTEIN GP. Top
16/7-6 is missing the   NORDLAND GP. Top
16/7-6 is missing the   HORDALAND GP. Top
16/7-6 is missing the   ROGALAND GP. Top
16/7-6 is missing the   SHETLAND GP. Top
17/4-1 is missing the   NORDLAND GP. Top
17/4-1 is missing the   HORDALAND GP. Top
17/4-1 is missing the   ROGALAND GP. Top
17/4-1 is missing the   SHETLAND GP. Top
17/4-1 is missing the   CROMER KNOLL GP. Top
17/4-1 is missing the   VIKING GP. Top
17/4-1 is missing the   ZECHSTEIN GP. Top
25/10-10   is up to date
25/10-9   is up to date
25/11-24   is up to date
25/5-3   is up to date
29/3-1   is up to date
31/2-10 is missing the   HORDALAND GP. Top
31/2-10 is missing the   ROGALAND GP. Top
31/2-10 is missing the   SHETLAND GP. Top
31/2-10 is missing the   CROMER KNOLL GP. Top
31/2-21 S   is up to date
34/10-16 R   is up to date
34/3-2 S is missing the   NORDLAND GP. Top
34/3-2 S is missing the   HORDALAND GP. Top
34/3-2 S is missing the   ROGALAND GP. Top
34/3-2 S is missing the   SHETLAND GP. Top
34/3-2 S is missing the   CROMER KNOLL GP. Top
34/3-2 S is missing the   DUNLIN GP. Top
34/3-3 A is missing the   NORDLAND GP. Top
34/3-3 A is missing the   HORDALAND GP. Top
34/3-3 A is missing the   ROGALAND GP. Top
34/6-1 S   is up to date
35/11-5 is missing the   HORDALAND GP. Top
35/11-5 is missing the   ROGALAND GP. Top
35/11-5 is missing the   SHETLAND GP. Top
35/11-5 is missing the   CROMER KNOLL GP. Top
35/11-5 is missing the   VIKING GP. Top
35/6-2 S   is up to date
35/9-7 is missing the   HORDALAND GP. Top
35/9-7 is missing the   ROGALAND GP. Top
35/9-7 is missing the   SHETLAND GP. Top
35/9-8   is up to date

Write missing tops to Petrel#

After identifying the missing Groups we can write them back to our Petrel project:

[7]:
mc.readonly = False
for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['Surface'], keep=False)
    curr_well=ptp.wells["Input/Wells/" + well_list[l] ]
    depth = mc.attributes["MD"]
    if len(missing) == 0:
        pass
    elif len(missing) == 1:
        marker = mc.stratigraphies[str(missing['Surface'].values[0])]
        mc.add_marker(well=curr_well,marker_stratigraphy=marker,measured_depth=float(missing['MD'].values[0]))
    else:
        for i in range(len(missing['Surface'])):
            marker = mc.stratigraphies[str(missing['Surface'].values[i])]
            mc.add_marker(well=curr_well,marker_stratigraphy=marker,measured_depth=float(missing['MD'].values[i]))

Running the cell above will write the missing Groups back to Petrel as shown in the gif bellow. The gif is not sped up .

welltops_Looped2.gif

Check again#

We can know compare the two dataframes again and make sure that we’ve added all the missing welltops to Petrel:

[8]:
mc = ptp.markercollections['Input/Groups']
df_mc = mc.as_dataframe(False)
newmc_df=df_mc[['Well identifier (Well name)','MD','Surface']]
newmc_df.head()
Gp_df=newmc_df[newmc_df['Surface'].str.contains('GP.')]
Gp_df=Gp_df.drop_duplicates()
Gp_df=Gp_df.sort_values(by=['Well identifier (Well name)'], ascending=[True])
Gp_df.rename(columns = {'Well identifier (Well name)':'Well'}, inplace = True)



for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['Surface'], keep=False)
    if len(missing) == 0:
        print(well_list[l],'  is up to date' )
    elif len(missing) == 1:
        print(well_list[l], 'is missing the  ', missing['Surface'].values[0])
    else:
        for i in range(len(missing['Surface'])):
            print(well_list[l], 'is missing the  ', missing['Surface'].values[i])
15/9-14   is up to date
15/9-23   is up to date
16/2-7   is up to date
16/7-6   is up to date
17/4-1   is up to date
25/10-10   is up to date
25/10-9   is up to date
25/11-24   is up to date
25/5-3   is up to date
29/3-1   is up to date
31/2-10   is up to date
31/2-21 S   is up to date
34/10-16 R   is up to date
34/3-2 S   is up to date
34/3-3 A   is up to date
34/6-1 S   is up to date
35/11-5   is up to date
35/6-2 S   is up to date
35/9-7   is up to date
35/9-8   is up to date

Check MD values#

So far we have added the missing welltops from NPD but we did not check if the existing ones have the correct measured depth:

[9]:
for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['MD','Surface'], keep=False)
    missing=missing.sort_values(by=['Surface'], ascending=[True])
    if len(missing) == 0:
        print(well_list[l],'  is up to date' )
    else:
        for i in range(0,len(missing['Surface']),2):
            print(well_list[l], '--- the ', missing['Surface'].values[i],"  has ",missing['MD'].values[i], "as MD on NPD and ", missing['MD'].values[i+1], " as MD in Petrel" )
15/9-14   is up to date
15/9-23 --- the  NORDLAND GP. Top   has  110.0 as MD on NPD and  50.0  as MD in Petrel
15/9-23 --- the  VESTLAND GP. Top   has  3087.0 as MD on NPD and  4000.0  as MD in Petrel
15/9-23 --- the  VIKING GP. Top   has  2938.0 as MD on NPD and  3500.0  as MD in Petrel
16/2-7 --- the  ROTLIEGEND GP. Top   has  2243.5 as MD on NPD and  3500.0  as MD in Petrel
16/7-6 --- the  CROMER KNOLL GP. Top   has  2504.0 as MD on NPD and  3000.0  as MD in Petrel
16/7-6 --- the  VIKING GP. Top   has  2555.0 as MD on NPD and  4000.0  as MD in Petrel
17/4-1 --- the  VESTLAND GP. Top   has  2265.0 as MD on NPD and  1500.0  as MD in Petrel
25/10-10 --- the  HORDALAND GP. Top   has  776.5 as MD on NPD and  777.0  as MD in Petrel
25/10-10 --- the  NORDLAND GP. Top   has  163.5 as MD on NPD and  164.0  as MD in Petrel
25/10-10 --- the  ROGALAND GP. Top   has  1767.5 as MD on NPD and  1768.0  as MD in Petrel
25/10-9   is up to date
25/11-24   is up to date
25/5-3   is up to date
29/3-1 --- the  BRENT GP. Top   has  3522.5 as MD on NPD and  3523.0  as MD in Petrel
29/3-1 --- the  NORDLAND GP. Top   has  156.2 as MD on NPD and  156.0  as MD in Petrel
31/2-10 --- the  VIKING GP. Top   has  1564.5 as MD on NPD and  1565.0  as MD in Petrel
31/2-21 S   is up to date
34/10-16 R --- the  BRENT GP. Top   has  3168.0 as MD on NPD and  3171.0  as MD in Petrel
34/10-16 R --- the  CROMER KNOLL GP. Top   has  2975.0 as MD on NPD and  2978.0  as MD in Petrel
34/10-16 R --- the  DUNLIN GP. Top   has  3475.0 as MD on NPD and  3472.0  as MD in Petrel
34/10-16 R --- the  HEGRE GP. Top   has  4007.0 as MD on NPD and  4010.0  as MD in Petrel
34/10-16 R --- the  HORDALAND GP. Top   has  1042.0 as MD on NPD and  1045.0  as MD in Petrel
34/10-16 R --- the  NORDLAND GP. Top   has  163.0 as MD on NPD and  160.0  as MD in Petrel
34/10-16 R --- the  ROGALAND GP. Top   has  1855.0 as MD on NPD and  1852.0  as MD in Petrel
34/10-16 R --- the  SHETLAND GP. Top   has  2059.0 as MD on NPD and  2062.0  as MD in Petrel
34/10-16 R --- the  VIKING GP. Top   has  3014.0 as MD on NPD and  3017.0  as MD in Petrel
34/3-2 S --- the  STATFJORD GP. Top   has  4273.0 as MD on NPD and  5000.0  as MD in Petrel
34/3-2 S --- the  VIKING GP. Top   has  3688.0 as MD on NPD and  1000.0  as MD in Petrel
34/3-3 A --- the  SHETLAND GP. Top   has  2090.0 as MD on NPD and  1500.0  as MD in Petrel
34/6-1 S --- the  BRENT GP. Top   has  3469.5 as MD on NPD and  3470.0  as MD in Petrel
34/6-1 S --- the  DUNLIN GP. Top   has  3658.5 as MD on NPD and  3659.0  as MD in Petrel
34/6-1 S --- the  NORDLAND GP. Top   has  406.5 as MD on NPD and  407.0  as MD in Petrel
34/6-1 S --- the  ROGALAND GP. Top   has  1877.5 as MD on NPD and  1878.0  as MD in Petrel
34/6-1 S --- the  VIKING GP. Top   has  3411.5 as MD on NPD and  3412.0  as MD in Petrel
35/11-5   is up to date
35/6-2 S   is up to date
35/9-7   is up to date
35/9-8 --- the  CROMER KNOLL GP. Top   has  2704.5 as MD on NPD and  2705.0  as MD in Petrel
35/9-8 --- the  SHETLAND GP. Top   has  1737.5 as MD on NPD and  1738.0  as MD in Petrel

Update MD values#

Now that we’ve identified each well that contains a well top at the wrong depth we can correct the MD values in Petrel:

[10]:
mc.readonly = False
for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['MD','Surface'], keep=False)
    missing=missing.sort_values(by=['Surface'], ascending=[True])
    curr_well=ptp.wells["Input/Wells/" + well_list[l] ]
    depth = mc.attributes["MD"]
    if len(missing) == 0:
        pass
    else:
        for i in range(0,len(missing['Surface']),2):
            marker = mc.stratigraphies[str(missing['Surface'].values[i])]
            depth.set_values(data=[missing['MD'].values[i]],marker_stratigraphy=marker,well=curr_well)

welltops_Looped3.gif

Check MD values again#

Finally we can check again to make sure all the Groups have the correct measured depth value:

[14]:
mc = ptp.markercollections['Input/Groups']
df_mc = mc.as_dataframe(False)
newmc_df=df_mc[['Well identifier (Well name)','MD','Surface']]
newmc_df.head()
Gp_df=newmc_df[newmc_df['Surface'].str.contains('GP.')]
Gp_df=Gp_df.drop_duplicates()
Gp_df=Gp_df.sort_values(by=['Well identifier (Well name)'], ascending=[True])
Gp_df.rename(columns = {'Well identifier (Well name)':'Well'}, inplace = True)

for l in range(len(well_list)):
    NPD_well=NPD_cut[NPD_cut['Well'].str.contains(well_list[l])]
    Ptd_well=Gp_df[Gp_df['Well'].str.contains(well_list[l])]
    missing = pd.concat([NPD_well,Ptd_well]).drop_duplicates(subset=['MD','Surface'], keep=False)
    missing=missing.sort_values(by=['Surface'], ascending=[True])
    if len(missing) == 0:
        print(well_list[l],'  is up to date' )
    else:
        for i in range(0,len(missing['Surface']),2):
            print(well_list[l], '--- the ', missing['Surface'].values[i],"  has ",missing['MD'].values[i], "as MD on NPD and ", missing['MD'].values[i+1], " as MD in Petrel" )
15/9-14   is up to date
15/9-23   is up to date
16/2-7   is up to date
16/7-6   is up to date
17/4-1   is up to date
25/10-10   is up to date
25/10-9   is up to date
25/11-24   is up to date
25/5-3   is up to date
29/3-1   is up to date
31/2-10   is up to date
31/2-21 S   is up to date
34/10-16 R   is up to date
34/3-2 S   is up to date
34/3-3 A   is up to date
34/6-1 S   is up to date
35/11-5   is up to date
35/6-2 S   is up to date
35/9-7   is up to date
35/9-8   is up to date