Source code for workflow

'''
Python module for petrophysics

'''

import pandas as pd
import numpy as np
import matplotlib.colors as colors
from matplotlib import pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from itertools import cycle
from random import choice
import warnings
warnings.filterwarnings('ignore')


[docs]class Quanti(object): r""" Class to petrophysics workflow to evaluate any number of reservoirs of interest. Computes IGR/VSH, Total and Effective Porosities, Water and Hydrocarbon Saturation, Permeability Attributes ----------- df : pd.DataFrame Dataframe of data zonename : list List of zonenames. Will be accessed from the zonation file passed into `Zonation` class ztop : list Tops of the reservoirs. Will be accessed from the zonation file passed into `Zonation` class zbot : list Bottoms of the reservoirs. Will be accessed from the zonation file passed into `Zonation` class f_mids : list Formation mids to help place the `zonename` in the plots. Will be accessed from the zonation file passed into `Zonation` class depth: str Depth column gr : str Gamma ray column rt : str Resistivity column nphi : str Neutron porosity column rhob : str Bulk density column sonic : str default None Sonic column (optional) use_mean : bool default None For cutoff. Whether to use mean of GR in IGR/VSH computation or not. If None, uses either median or average value use_median; bool default None For cutoff. Whether to use median of GR in IGR/VSH computation or not. If None, uses either mean or average value Example -------- >>> #loading libraries/packages >>> from petrolib.workflow import Quanti >>> from petrolib.plot import Zonation >>> from petrolib.file_reader import load_las >>> from pathlib import Path >>> #loading well file and zonation/tops file >>> well_path = Path(r"./15_9-F-1A.LAS") >>> contact_path = Path(r"./well tops.csv") >>> las, df= load_las(well_path, curves=['GR', 'RT', 'NPHI', 'RHOB'], return_las=True) >>> #creating zonation class to extra info >>> zones = Zonation(df, path=contact_path) >>> ztop, zbot, zn, fm = zones() >>> #creating quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB', use_mean=True) """ def __init__(self, df:pd.DataFrame, zonename:list, ztop:list, zbot:list, f_mids:list, depth:str, gr:str, rt:str, nphi:str, rhob:str, sonic:str=None, use_mean:bool=None, use_median:bool=None): self._df = df self._depth = depth self._use_mean = use_mean self._use_median = use_median self._gr = gr self._rt = rt self._nphi = nphi self._rhob = rhob self._sonic = sonic self._ztop = ztop self._zbot = zbot self._zonename=zonename self._f_mids = f_mids def __call__(self): ''' Returns ------- method to return the GR_matrix, GR_Shale, GR_Sand and Zone_Names. Can be invoked if the instance of the Quanti object is called Example ------- >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB', use_mean=True) >>> x = pp() ''' return self._results def _filter(self): ''' A private method to filter/restrict the data to the zones of interest This also determines GR_matrix, GR_Shale and GR_Sand of the reservoirs Returns ------- Return two objects. Dataframe containing the parameters for VSH computation and filtered Dataframe ''' # columns=self._df.columns.tolist() data = list() no = range(0,len(self._ztop)+1) for i, z in zip(no, self._zonename): d= self._df[(self._df[self._depth] >= self._ztop[i]) & (self._df[self._depth] <= self._zbot[i])] # d['Zone'] = z data.append(d) #get the parameters for each zone self._grMatrix = list() self._grSand = list() self._grShale = list() self._zone = list() for d, z in zip(data, self._zonename): if self._use_mean==None and self._use_median==None: self._grMatrix.append(75.) elif self._use_mean == True and self._use_median==None: self._grMatrix.append(d[self._gr].mean()) elif self._use_mean == None and self._use_median==True: self._grMatrix.append(d[self._gr].median()) self._grShale.append(d[self._gr].max()) self._grSand.append(d[self._gr].min()) self._zone.append(z) #store parameter info in dataframe df = {'GR_Matrix': self._grMatrix, 'GR_Shale': self._grShale, 'GR_Sand' : self._grSand, 'Zone' : self._zone} self._results = pd.DataFrame.from_records(df) #return result return self._results, data
[docs] def vshale(self, method:str='linear', show_plot:bool=False, palette_op:str=None, figsize:tuple=None): ''' Computes the Volume of Shale Parameters ---------- method : str default 'linear' Volume of Shale method. {'linear', 'clavier', 'larionov_ter', 'larionov_older', 'stieber_1', 'stieber_2, 'stieber_m_pliocene'} * Linear = Gamma Ray Index (IGR) * Larionov Tertiary * Larionov Older Rocks * Stieber (Miocene/Pliocene) show_plot : bool default False Display plot if True.. Plots GR, VSH and Zone track palette_op: str default None Palette option for to color code vshale plot. Check https://matplotlib.org/stable/tutorials/colors/colormaps.html figsize: tuple default None Size of plot Returns ------ Either/Both Dataframe containing the VSH and the plot if show_plot=True Example ------- >>> # create Quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB') >>> # display plot only >>> pp.vshale(method='clavier', show_plot=True, palette_op='cubehelix', figsize=(9,12)) >>> # display data only >>> x = pp.vshale(method='clavier') >>> x = pd.concat(x) >>> print(x) ''' self._palette = palette_op self._v_method = method self._fig = figsize results, data = self._filter() new_data = list() for idx, d in enumerate(data): d['VShale'] = [(i - (results['GR_Sand'])[idx])/(results['GR_Shale'][idx]-results['GR_Sand'][idx]) for i in d[self._gr]] if method == 'clavier': d['VShale'] = 1.7 - np.sqrt((3.38 - (d['VShale'] + .7)**2)) new_data.append(d) elif method == 'larionov_ter': d['VShale'] = .083*((2 ** (3.7*d['VShale'])) - 1) new_data.append(d) elif method == 'larionov_older': d['VShale'] = .33 *((2 ** (2*d['VShale'])) - 1) new_data.append(d) elif method == 'stieber_1': d['VShale'] = d['VShale'] / (2 - d['VShale']) new_data.append(d) elif method == 'stieber_2': d['VShale'] = d['VShale'] / (4 - (3 * d['VShale'])) new_data.append(d) elif method == 'stieber_m_pliocene': d['VShale'] = d['VShale'] / (3 - (2 * d['VShale'])) new_data.append(d) elif method=='linear': new_data.append(d) # PLOT data = pd.concat(new_data).reindex(np.arange(0, self._df.shape[0])) if show_plot == True: assert palette_op != None and figsize != None, f'Supply value for palette option and figsize' fig, ax = plt.subplots(nrows=1, ncols=3, figsize=figsize, sharey=True) fig.suptitle(f'Volume of Shale', size=15, y=1.) span = 1 cmap=plt.get_cmap(palette_op) color_index = np.arange(0, 1, span/10) logs = [self._gr, 'VShale'] gr_base = 75.#(data[self._gr].max() - data[self._gr].min())/2 for i in range(2): ax[i].plot(data[logs[i]], data[self._depth], color='black', linewidth=0.5) if i == 1: for index in sorted(color_index): index_value = (index-0.)/span palette = cmap(index_value) ax[i].fill_betweenx(data[self._depth], 0., data['VShale'], where=data['VShale']>=index, color=palette) # ax[i].set_xlim(0, 1) elif i == 0: ax[i].fill_betweenx(data[self._depth], gr_base, data[self._gr], where=data[self._gr]<=gr_base, facecolor='yellow', linewidth=0) ax[i].fill_betweenx(data[self._depth],data[self._gr], gr_base, where=data[self._gr]>=gr_base, facecolor='brown', linewidth=0) ax[i].set_title(logs[i], pad=15) ax[i].minorticks_on() ax[i].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[i].invert_yaxis() ax[i].grid(which='major', linestyle='-', linewidth=1.0, color='darkgrey') ax[i].grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[i].tick_params(axis='x') ax[i].spines['top'].set_edgecolor('black') ax[i].spines["top"].set_position(("axes", 1.02)); ax[i].xaxis.set_ticks_position("top") ax[i].xaxis.set_label_position("top") ax[i].hlines([t for t in self._ztop], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) ax[i].hlines([b for b in self._zbot], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) #formation subplot ax[-1].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[-1].invert_yaxis() ax[-1].set_title('Zones', pad=45) ax[-1].set_xticks([]) # ax[-1].set_yticklabels([]) ax[-1].set_xticklabels([]) ax[-1].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) ax[-1].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) formations = ax[-1] #delineating zones cycol = cycle('bgrycmk') color = [choice(next(cycol)) for i in range(len(self._zonename))] np.random.shuffle(color) for i in ax: for t,b, c in zip(self._ztop, self._zbot, color): i.axhspan(t, b, color=c, alpha=0.3) #adding zone names for label, fm_mids in zip(self._zonename, self._f_mids): formations.text(0.5, fm_mids, label, rotation=0, verticalalignment='center', fontweight='bold', fontsize='large') # plt.tight_layout(h_pad=1) fig.subplots_adjust(wspace = 0.01) # plt.show() return new_data elif show_plot==False: # assert palette_op == None and figsize == None, f'show_plot is set to {show_plot}. Set palette_op and figsize as None' return new_data
[docs] def porosity(self, method:str='density', rhob_shale:float=2.4, rhob_fluid:float=1., rhob_matrix:float=2.65, fzs:float=None, show_plot:bool=False, figsize:tuple=None): ''' Computes the effective and total porosities using the 'density' and Wyllie's 'sonic' method. To use, must have called the `vshale` method Parameters ---------- method : str default 'density' Porosity method. {'density', 'sonic'} rhob_shale : float default 2.4 Shale matrix rhob_fluid : float default 1.0 Fluid density rhob_matrix : float default 2.65 Matrix density fzs: float default None Flushed zone saturation for PHIE. If None, it is calculated from rhob_fluid, rhob_shale and rhob_matrix show_plot : bool default False Display plot if True.. Plots RHOB, VSH, PHIE/PHIT and Zone track figsize: tuple default None Size of plot Returns ------- Either/Both Dataframe containing the PHIE/PHIT and the plot if show_plot=True Example ------- >>> # create Quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB') >>> # display plot only >>> pp.porosity(method='density', show_plot=True, figsize=(10, 12)) >>> # display data only >>> y = pp.porosity(method='density') >>> result = pd.concat(y) >>> print(result) ''' # self._show_plot == show_plot self._p_method = method #calls the vshale method and concate all the dataframe representing each zones new_data = self.vshale(method=self._v_method, palette_op=self._palette, figsize=self._fig) for d in new_data: #equations from Techlog manual if method == 'density': d['PHIT'] = (rhob_matrix - d[self._rhob])/ (rhob_matrix - rhob_fluid) #total porosity d['PHIT'] = d['PHIT'].mask(d['PHIT']<0, 0)#mask area where phit is less than 0 if fzs != None: phi_fzs = fzs else: phi_fzs = (rhob_matrix - rhob_shale)/ (rhob_matrix - rhob_fluid) #flushed zone saturation d['PHIE'] = d['PHIT'] - (phi_fzs * d['VShale']) #effective porosity d['PHIE'] = d['PHIE'].mask(d['PHIE']<0, 0)#mask areas where phie is less than 0 elif method == 'sonic': ''' only supports Wyllie equation ''' d['PHIT'] = (d[self._sonic] - 47.) / (189. - 47.) d['PHIT'] = d['PHIT'].mask(d['PHIT']<0, 0)#mask areas where phie is less than 0 d['PHIE'] = 0 data = pd.concat(new_data).reindex(np.arange(0, self._df.shape[0])) if show_plot == True: assert figsize != None, f'Supply figsize value' fig, ax = plt.subplots(nrows=1, ncols=4, figsize=figsize, sharey=True) fig.suptitle(f'Porosity', size=15, y=1.) span = 1 cmap=plt.get_cmap(self._palette) #uses palette from vshale color_index = np.arange(0, 1, span/10) #for RHOB and VSH logs = [self._rhob, 'VShale'] for i in range(2): if i==0: ax[i].plot(data[logs[i]], data[self._depth], color='blue', linewidth=1.) if i == 1: ax[i].plot(data[logs[i]], data[self._depth], color='black', linewidth=0.3) for index in sorted(color_index): index_value = (index-0.)/span palette = cmap(index_value) ax[i].fill_betweenx(data[self._depth], 0., data['VShale'], where=data['VShale']>=index, color=palette) ax[i].set_title(logs[i], pad=15) ax[i].minorticks_on() ax[i].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[i].invert_yaxis() ax[i].grid(which='major', linestyle='-', linewidth=1.0, color='darkgrey') ax[i].grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[i].tick_params(axis='x') ax[i].spines['top'].set_edgecolor('black') ax[i].spines["top"].set_position(("axes", 1.02)); ax[i].xaxis.set_ticks_position("top") ax[i].xaxis.set_label_position("top") ax[i].hlines([t for t in self._ztop], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) ax[i].hlines([b for b in self._zbot], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) #for PHIT ax[2].minorticks_on() ax[2].set_xticklabels([]);ax[2].set_xticks([]) ax[2].yaxis.grid(which='major', linestyle='-', linewidth=1, color='darkgrey') ax[2].yaxis.grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[2].hlines([t for t in self._ztop], xmin=0, xmax=data["PHIT"].max(), colors='black', linestyles='solid', linewidth=1.) ax[2].hlines([b for b in self._zbot], xmin=0, xmax=data["PHIT"].max(), colors='black', linestyles='solid', linewidth=1.) nphi_ = ax[2].twiny() nphi_.grid(which='major', linestyle='-', linewidth=0.5, color='darkgrey') nphi_.plot(data['PHIT'], data[self._depth], color='blue', linewidth=0.5) nphi_.fill_betweenx(data[self._depth], data['PHIE'].max(), data['PHIT'], color='slategray', linewidth=1.) nphi_.set_xlim(data['PHIT'].min(), data['PHIT'].max()) nphi_.set_ylim(data[self._depth].min(), data[self._depth].max()) nphi_.invert_yaxis() nphi_.invert_xaxis() nphi_.tick_params(axis='x', colors='blue') nphi_.spines['top'].set_edgecolor('blue') nphi_.set_xlabel('PHIT_'+method[0].upper(), color='blue') nphi_.spines["top"].set_position(("axes", 1.02)) nphi_.xaxis.set_ticks_position("top") nphi_.xaxis.set_label_position("top") nphi_.set_xticks(list(np.linspace(data['PHIT'].min(), data['PHIT'].max(), num=4))) #for PHIE phi = ax[2].twiny() phi.plot(data['PHIE'], data[self._depth], color='red', linewidth=0.5) phi.fill_betweenx(data[self._depth], data["PHIE"], data['PHIE'].max(), color='lightblue') phi.set_xlim(data['PHIE'].min(), data['PHIE'].max()) phi.set_ylim(data[self._depth].min(), data[self._depth].max()) phi.invert_yaxis() phi.invert_xaxis() phi.xaxis.label.set_color('red') phi.tick_params(axis='x', colors='red') phi.spines['top'].set_edgecolor('red') phi.set_xlabel('PHIE_'+method[0].upper(), color='red') phi.spines["top"].set_position(("axes", 1.05)) phi.xaxis.set_ticks_position("top") phi.xaxis.set_label_position("top") phi.set_xticks(list(np.linspace(data['PHIE'].min(), data['PHIE'].max(), num=4))) #formation subplot ax[-1].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[-1].invert_yaxis() ax[-1].set_title('Zones', pad=45) ax[-1].set_xticks([]) # ax[-1].set_yticklabels([]) ax[-1].set_xticklabels([]) ax[-1].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) ax[-1].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) formations = ax[-1] #delineating zones cycol = cycle('bgrycmk') color = [choice(next(cycol)) for i in range(len(self._zonename))] np.random.shuffle(color) for i in ax: for t,b, c in zip(self._ztop, self._zbot, color): i.axhspan(t, b, color=c, alpha=0.3) #adding zone names for label, fm_mids in zip(self._zonename, self._f_mids): formations.text(0.5, fm_mids, label, rotation=0, verticalalignment='center', fontweight='bold', fontsize='large') plt.tight_layout(h_pad=1) fig.subplots_adjust(wspace = 0.01) # plt.show() return new_data elif show_plot==False: # assert figsize == None, f'show_plot is set to {show_plot}. Set figsize as None' return new_data
[docs] def water_saturation(self, method:str='archie', rw:float=0.03, a:float=1., m:float=2., n:float=2., show_plot:bool=False, figsize:tuple=None): ''' Computes water and hydrocarbon saturation To use, must have called both `vshale` and `porosity` methods Parameters ---------- method : str default 'archie' Water Saturation method. {'archie', 'simmandoux'} rw : float default 0.03 Formation water resisitivity a : float default, 1. Turtuosity factor m : float default 2. Cementation factor n : float default 2. Saturation exponent show_plot : bool default False Display plot if True.. Plots RT, SW, PHIE/PHIT and Zone track figsize: tuple default None Size of plot Returns ------ Either/Both Dataframe containing the SW, SH and the plot if show_plot=True Example ------- >>> # create Quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB') >>> # display plot only >>> pp.water_saturation(method='archie', show_plot=True, figsize=(10, 12)) >>> # display data only >>> z = pp.water_saturation(method='archie') >>> result = pd.concat(z) >>> print(result) ''' self._sw_method = method new_data = self.porosity(method=self._p_method) for d in new_data: # inspired from https://github.com/andymcdgeo/Petrophysics-Python-Series/blob/master/05%20-%20Petrophysical%20Calculations.ipynb if method == 'archie': d['SW'] = ((a/(d['PHIE']**m)) * (rw/d['RT']))**(a/n) #mask value greater than one to 1 d['SW'] = d['SW'].mask(d['SW']>1, 1) # d['SW'] = d['SW'].mask(d['SW']<0, 0) d['SH'] = 1 - d['SW'] elif method == 'simmandoux': A = (1 - d['VShale']) * a * rw / (d['PHIE'] ** m) B = A * d['VShale'] / (2 * 2) C = A / d['RT'] d['SW'] = ((B **2 + C)**0.5 - B) **(2 / n) #mask value greater than one to 1 d['SW'] = d['SW'].mask(d['SW']>1, 1) # d['SW'] = d['SW'].mask(d['SW']<0, 0) d['SH'] = 1 - d['SW'] data = pd.concat(new_data).reindex(np.arange(0, self._df.shape[0])) if show_plot == True: assert figsize != None, f'Supply figsize value' logs = [self._rt, 'SW'] fig, ax = plt.subplots(nrows=1, ncols=4, figsize=figsize, sharey=True) fig.suptitle(f'Saturations', size=15, y=1.) for i in range(2): # for resistivity plot if i==0: ax[i].semilogx(data[logs[i]], data[self._depth], color='red', linewidth=1, linestyle='--') ax[i].hlines([t for t in self._ztop], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) ax[i].hlines([b for b in self._zbot], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) #for non-resistivity plot if i==1: ax[i].plot(data[logs[i]], data[self._depth], color='blue', linewidth=0.5) ax[i].invert_xaxis() ax[i].set_xlim(1., 0.) ax[i].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid') ax[i].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid') ax[i].fill_betweenx(data[self._depth], data[logs[i]].max(), data[logs[i]], where=data[logs[i]]<=data[logs[i]].max(), facecolor='lightblue', interpolate=True, linewidth=0) ax[i].set_title(logs[i], pad=15) ax[i].minorticks_on() ax[i].set_ylim(data[self._depth].min(), data[self._depth].max()) ax[i].invert_yaxis() ax[i].grid(which='major', linestyle='-', linewidth=1.0, color='darkgrey') ax[i].grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[i].tick_params(axis='x') ax[i].spines['top'].set_edgecolor('black') ax[i].spines["top"].set_position(("axes", 1.02)); ax[i].xaxis.set_ticks_position("top") ax[i].xaxis.set_label_position("top") #for PHIT ax[2].minorticks_on() ax[2].set_xticklabels([]);ax[2].set_xticks([]) ax[2].yaxis.grid(which='major', linestyle='-', linewidth=1, color='darkgrey') ax[2].yaxis.grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[2].hlines([t for t in self._ztop], xmin=data["PHIT"].min(), xmax=data["PHIT"].max(), colors='black', linestyles='solid', linewidth=1.) ax[2].hlines([b for b in self._zbot], xmin=data["PHIT"].min(), xmax=data["PHIT"].max(), colors='black', linestyles='solid', linewidth=1.) nphi_ = ax[2].twiny() nphi_.grid(which='major', linestyle='-', linewidth=0.5, color='darkgrey') nphi_.plot(data['PHIT'], data[self._depth], color='blue', linewidth=0.5) nphi_.fill_betweenx(data[self._depth], data['PHIE'].max(), data['PHIT'], color='slategray', linewidth=1.) nphi_.set_xlim(data['PHIT'].min(), data['PHIT'].max()) nphi_.set_ylim(data[self._depth].min(), data[self._depth].max()) nphi_.invert_yaxis() nphi_.invert_xaxis() nphi_.tick_params(axis='x', colors='blue') nphi_.spines['top'].set_edgecolor('blue') nphi_.set_xlabel('PHIT_'+method[0].upper(), color='blue') nphi_.spines["top"].set_position(("axes", 1.02)) nphi_.xaxis.set_ticks_position("top") nphi_.xaxis.set_label_position("top") nphi_.set_xticks(list(np.linspace(data['PHIT'].min(), data['PHIT'].max(), num=5))) #for PHIE phi = ax[2].twiny() phi.plot(data['PHIE'], data[self._depth], color='red', linewidth=0.5) phi.fill_betweenx(data[self._depth], data["PHIE"], data['PHIE'].max(), color='lightblue') phi.set_xlim(data['PHIE'].min(), data['PHIE'].max()) phi.set_ylim(data[self._depth].min(), data[self._depth].max()) phi.invert_yaxis() phi.invert_xaxis() phi.xaxis.label.set_color('red') phi.tick_params(axis='x', colors='red') phi.spines['top'].set_edgecolor('red') phi.set_xlabel('PHIE_'+method[0].upper(), color='red') phi.spines["top"].set_position(("axes", 1.05)) phi.xaxis.set_ticks_position("top") phi.xaxis.set_label_position("top") phi.set_xticks(list(np.linspace(data['PHIE'].min(), data['PHIE'].max(), num=5))) #formation subplot ax[-1].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[-1].invert_yaxis() ax[-1].set_title('Zones', pad=45) ax[-1].set_xticks([]) # ax[-1].set_yticklabels([]) ax[-1].set_xticklabels([]) ax[-1].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) ax[-1].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) formations = ax[-1] #delineating zones cycol = cycle('bgrcmk') color = [choice(next(cycol)) for i in range(len(self._zonename))] np.random.shuffle(color) for i in ax: for t,b, c in zip(self._ztop, self._zbot, color): i.axhspan(t, b, color=c, alpha=0.3) #adding zone names for label, fm_mids in zip(self._zonename, self._f_mids): formations.text(0.5, fm_mids, label, rotation=0, verticalalignment='center', fontweight='bold', fontsize='large') # plt.tight_layout(h_pad=1) fig.subplots_adjust(wspace = 0.01) # plt.show() return new_data elif show_plot==False: # assert figsize == None, f'show_plot is set to {show_plot}. Set figsize as None' return new_data
[docs] def permeability(self, show_plot:bool=False, figsize:tuple=None): ''' Computes the permeability. To use, must have called `vshale` and `porosity` and `water_saturation` methods Parameters ---------- show_plot : bool default False Display plot if True.. Plots PHIE, Permeability and Zone track figsize: tuple default None Size of plot Returns ------ Either/Both Dataframe containing the Perm and the plot if show_plot=True Example ------- >>> # create Quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB') >>> # display plot only >>> pp.vshale(method='clavier') >>> pp.porosity(method='density') >>> pp.water_saturation(method='archie') >>> pp.permeability(show_plot=True, figsize=(9, 10)) >>> # display data only >>> x = pp.vshale(method='clavier') >>> y = pp.porosity(method='density') >>> z = pp.water_saturation(method='archie') >>> a = pp.permeability() >>> result = pd.concat(a) >>> print(result) ''' new_data = self.water_saturation(method=self._sw_method) for d in new_data: d['Perm'] = 307. + (26552*pow(d['PHIE'], 2)) - (34540 * pow(d['PHIE'] * d['SW'], 2)) d['Perm'] = d['Perm'].mask(d['Perm']<0, 0) data = pd.concat(new_data).reindex(np.arange(0, self._df.shape[0])) if show_plot == True: assert figsize != None, f'Supply figsize value' logs = ['PHIE', 'Perm'] fig, ax = plt.subplots(nrows=1, ncols=3, figsize=figsize, sharey=True) fig.suptitle(f'Permeability', size=15, y=1.) for i in range(2): # for non-resistivity plot if i==1: ax[i].semilogx(data[logs[i]], data[self._depth], color='blue', linewidth=1.0) # ax[0].set_xlim(0.1, 1000) if i==0: ax[i].plot(data[logs[i]], data[self._depth], color='blue', linewidth=0.5) ax[i].invert_xaxis() ax[i].fill_betweenx(data[self._depth], data[logs[i]].max(), data[logs[i]], where=data[logs[i]]<=data[logs[i]].max(), facecolor='lightblue', interpolate=True, linewidth=0) ax[i].set_title(logs[i], pad=15) ax[i].minorticks_on() ax[i].set_ylim(data[self._depth].min(), data[self._depth].max()) ax[i].invert_yaxis() ax[i].grid(which='major', linestyle='-', linewidth=1.0, color='darkgrey') ax[i].grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[i].tick_params(axis='x') ax[i].spines['top'].set_edgecolor('black') ax[i].spines["top"].set_position(("axes", 1.02)); ax[i].xaxis.set_ticks_position("top") ax[i].xaxis.set_label_position("top") ax[i].hlines([t for t in self._ztop], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) ax[i].hlines([b for b in self._zbot], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) #formation subplot ax[-1].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[-1].invert_yaxis() ax[-1].set_title('Zones', pad=45) ax[-1].set_xticks([]) # ax[-1].set_yticklabels([]) ax[-1].set_xticklabels([]) ax[-1].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) ax[-1].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) formations = ax[-1] #delineating zones cycol = cycle('bgrycmk') color = [choice(next(cycol)) for i in range(len(self._zonename))] np.random.shuffle(color) for i in ax: for t,b, c in zip(self._ztop, self._zbot, color): i.axhspan(t, b, color=c, alpha=0.3) #adding zone names for label, fm_mids in zip(self._zonename, self._f_mids): formations.text(0.5, fm_mids, label, rotation=0, verticalalignment='center', fontweight='bold', fontsize='large') plt.tight_layout(h_pad=1) fig.subplots_adjust(wspace = 0.01) # plt.show() return new_data elif show_plot==False: # assert figsize == None, f'show_plot is set to {show_plot}. Set figsize as None' return new_data
[docs] def flags(self, vsh_cutoff:float, por_cutoff:float, sw_cutoff:float, ref_unit:str='m', show_plot:bool=False, palette_op:str=None, figsize:tuple=None): ''' Create the {ROCK, RES, PAY} flags To use, must have called `vshale`, `porosity`, `water_saturation` and `permeability` methods Parameters ---------- vsh_method : float Volume of Shale cutoff. Applied only to ['ROCK'] flag por_cutoff : float Porosity cutoff. Applied only to the ['ROCK', 'RES'] flags sw_cutoff : float Water Saturation cutoff. Applied only to the ['ROCK', 'RES', 'PAY] flags ref_unit : str default 'm' Reference unit for measured depth. Defaults to metres show_plot : bool default False Display plot if True.. Plots GR, RT, VSH, SW, Perm, NPHI/RHOB, PHIE/PHIT, ['ROCK', 'RES', 'PAY] flags and Zonation track palette_op : str default None palette option for VSH coloring. Check https://matplotlib.org/stable/tutorials/colors/colormaps.html for availabel palette options figsize: tuple default None Size of plot Returns ------ Either/Both Dataframe containing the flags and the plot if show_plot=True Example ------- >>> # Create Quanti class >>> pp = Quanti(df, zn, ztop, zbot, fm, 'DEPTH', 'GR', 'RT', 'NPHI', 'RHOB') >>> # Display plot only >>> pp.flags(por_cutoff=.12, vsh_cutoff=.5, sw_cutoff=0.8, show_plot=True, palette_op='cubehelix', figsize=(20, 15)) >>> # Display data only >>> y = pp.flags(por_cutoff=.12, vsh_cutoff=.5, sw_cutoff=0.8) >>> result = pd.concat(y) >>> print(result) ''' self._vsh_cutoff = vsh_cutoff self._sw_cutoff = sw_cutoff self._por_cutoff = por_cutoff self._pale = palette_op self._ref = ref_unit self.show = show_plot new_data = self.permeability() #creating flags # 1 for net and 0 for gross for d in new_data: d['ROCK_NET_FLAG'] = [1 if (j < vsh_cutoff) else 0 for j in d['VShale']] d['RES_NET_FLAG'] = [1 if (i > por_cutoff) or (j < vsh_cutoff) else 0 for i, j in zip(d['PHIE'], d['VShale'])] d['PAY_NET_FLAG'] = [1 if (i > por_cutoff) or (j < vsh_cutoff) or (k < sw_cutoff) else 0 for i, j, k in zip(d['PHIE'], d['VShale'], d['SW'])] data = pd.concat(new_data).reindex(np.arange(0, self._df.shape[0])) data2 = pd.concat(new_data) #creating plots if show_plot == True: assert palette_op != None and figsize != None, f'Supply palette and figsize values' fig, ax = plt.subplots(nrows=1, ncols=11, figsize=figsize) fig.suptitle(f'Well Layout', size=15, y=1.) #creating netpay flag facies_colors = ['#F4D03F', '#004347']; facies_labels = ['Gross Pay', 'Net Pay'] facies_colormap = {} for ind, label in enumerate(facies_labels): facies_colormap[label] = facies_colors[ind] cmap_facies = colors.ListedColormap( facies_colors[0 : 2], 'indexed' ) # data = data.set_index('DEPTH') #.loc[(data.DEPTH >=self._ztop[0])&(data.DEPTH <=self._zbot[-1])] # .loc[(data.DEPTH >= data.DEPTH.min())&(data.DEPTH <=data.DEPTH.max())] cluster1 = np.repeat(np.expand_dims(data['ROCK_NET_FLAG'].values, 1), 100, 1) cluster1 = pd.DataFrame(cluster1) cluster1 = cluster1.loc[(cluster1.index>=(data2[self._depth].index.min())) & (cluster1.index<=(data2[self._depth].index.max()))] cluster2 = np.repeat(np.expand_dims(data['RES_NET_FLAG'].values, 1), 100, 1) cluster2 = pd.DataFrame(cluster2) cluster2 = cluster2.loc[(cluster2.index>=(data2[self._depth].index.min())) & (cluster2.index<=(data2[self._depth].index.max()))] cluster3 = np.repeat(np.expand_dims(data['PAY_NET_FLAG'].values, 1), 100, 1) cluster3 = pd.DataFrame(cluster3) cluster3 = cluster3.loc[(cluster3.index>=(data2[self._depth].index.min())) & (cluster3.index<=(data2[self._depth].index.max()))] #logs to plot logs = [self._gr, self._rt, 'VShale', 'SW', 'Perm'] #generate random colors cycol = cycle('bgrycmk') span = 1 cmap=plt.get_cmap(self._pale) color_index = np.arange(0, 1, span/10) gr_base = 75.#(data[self._gr].max() - data[self._gr].min())/2 #numeric plots for i in range(len(logs)): #resistivity if i == 1 or i ==4: ax[i].semilogx(data[logs[i]], data[self._depth], color=next(cycol), linestyle='--') #non-resistivity else: ax[i].plot(data[logs[i]], data[self._depth], color=next(cycol), linewidth=0.5) if i == 2: for index in sorted(color_index): index_value = (index-0.)/span palette = cmap(index_value) ax[i].fill_betweenx(data[self._depth], 0., data['VShale'], where=data['VShale']>=index, color=palette) elif i == 0: ax[i].set_xlim(data[logs[i]].min(), data[logs[i]].max()) ax[i].fill_betweenx(data[self._depth], gr_base, data[self._gr], where=data[self._gr]<=gr_base, facecolor='yellow', linewidth=0) ax[i].fill_betweenx(data[self._depth],data[self._gr], gr_base, where=data[self._gr]>=gr_base, facecolor='brown', linewidth=0) if i > 0: ax[i].set_yticklabels([]) if i ==3: ax[i].invert_xaxis() ax[i].fill_betweenx(data[self._depth], data[logs[i]].max(), data[logs[i]], where=data[logs[i]]<=data[logs[i]].max(), facecolor='lightblue', interpolate=True, linewidth=0) #facies = data['ROCK_NET_FLAG'] # F = np.vstack((facies,facies)).T # ax[i].imshow(F, aspect='auto', extent=[0,1,max(data.DEPTH), min(data.DEPTH)]) ax[i].set_title(logs[i], pad=15) ax[i].minorticks_on() ax[i].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[i].invert_yaxis() ax[i].grid(which='major', linestyle='-', linewidth=1.0, color='darkgrey') ax[i].grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[i].xaxis.label.set_color(next(cycol)) ax[i].tick_params(axis='x', colors=next(cycol)) ax[i].spines['top'].set_edgecolor(next(cycol)) ax[i].spines["top"].set_position(("axes", 1.02)); ax[i].xaxis.set_ticks_position("top") ax[i].xaxis.set_label_position("top") ax[i].hlines([t for t in self._ztop], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) ax[i].hlines([b for b in self._zbot], xmin=data[logs[i]].min(), xmax=data[logs[i]].max(), colors='black', linestyles='solid', linewidth=1.) #for rhob ax[5].minorticks_on() ax[5].yaxis.grid(which='major', linestyle='-', linewidth=1, color='darkgrey') ax[5].yaxis.grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[5].set_yticklabels([]) ax[5].set_xticklabels([]);ax[5].set_xticks([]) nphi_ = ax[5].twiny() nphi_.grid(which='major', linestyle='-', color='darkgrey') nphi_.plot(data[self._rhob], data[self._depth], color='red', linestyle='-', linewidth=0.5) nphi_.set_xlim(data[self._rhob].min(), data[self._rhob].max()) nphi_.set_ylim(data[self._depth].min(), data[self._depth].max()) nphi_.invert_yaxis() nphi_.xaxis.label.set_color('red') nphi_.tick_params(axis='x', colors='red') nphi_.spines['top'].set_edgecolor('red') nphi_.set_xlabel(self._rhob, color='red') nphi_.spines["top"].set_position(("axes", 1.02)) nphi_.xaxis.set_ticks_position("top") nphi_.xaxis.set_label_position("top") nphi_.set_xticks(list(np.linspace(data[self._rhob].min(), data[self._rhob].max(), num=3))) #for nphi rhob_ = ax[5].twiny() rhob_.plot(data[self._nphi], data[self._depth], 'b--', linewidth=0.5) rhob_.invert_xaxis() rhob_.set_xlim(data[self._nphi].max(), data[self._nphi].min()) rhob_.set_ylim(data[self._depth].min(), data[self._depth].max()) rhob_.invert_yaxis() rhob_.xaxis.label.set_color('blue') rhob_.tick_params(axis='x', colors='blue') rhob_.spines['top'].set_edgecolor('blue') rhob_.set_xlabel(self._nphi, color='blue') rhob_.spines["top"].set_position(("axes", 1.05)) rhob_.xaxis.set_ticks_position("top") rhob_.xaxis.set_label_position("top") rhob_.set_xticks(list(np.linspace(data[self._nphi].min(), data[self._nphi].max(), num=3))) #setting up the nphi and rhob fill #inspired from x2=data[self._rhob] x1=data[self._nphi] x = np.array(rhob_.get_xlim()) z = np.array(nphi_.get_xlim()) nz=((x2-np.max(z))/(np.min(z)-np.max(z)))*(np.max(x)-np.min(x))+np.min(x) #shows both porous and non-porous zones rhob_.fill_betweenx(data[self._depth], x1, nz, where=x1<=nz, interpolate=True, color='yellow', linewidth=0) rhob_.fill_betweenx(data[self._depth], x1, nz, where=x1>=nz, interpolate=True, color='brown', linewidth=0) #for PHIT ax[6].minorticks_on() ax[6].set_yticklabels([]) ax[6].set_xticklabels([]);ax[6].set_xticks([]) ax[6].yaxis.grid(which='major', linestyle='-', linewidth=1, color='darkgrey') ax[6].yaxis.grid(which='minor', linestyle='-', linewidth=0.5, color='lightgrey') ax[6].hlines([t for t in self._ztop], xmin=data["PHIT"].min(), xmax=data["PHIT"].max(), colors='black', linestyles='solid') ax[6].hlines([b for b in self._zbot], xmin=data["PHIT"].min(), xmax=data["PHIT"].max(), colors='black', linestyles='solid') phi_ = ax[6].twiny() phi_.grid(which='major', linestyle='-', linewidth=0.5, color='darkgrey') phi_.plot(data['PHIT'], data[self._depth], color='blue', linewidth=0.5) phi_.fill_betweenx(data[self._depth], data['PHIE'].max(), data['PHIT'], color='slategray', linewidth=1.) phi_.set_xlim(data['PHIT'].min(), data['PHIT'].max()) phi_.set_ylim(data[self._depth].min(), data[self._depth].max()) phi_.invert_yaxis() phi_.invert_xaxis() phi_.tick_params(axis='x', colors='blue') phi_.spines['top'].set_edgecolor('blue') phi_.set_xlabel('PHIT', color='blue') phi_.spines["top"].set_position(("axes", 1.02)) phi_.xaxis.set_ticks_position("top") phi_.xaxis.set_label_position("top") phi_.set_xticks(list(np.linspace(data['PHIT'].min(), data['PHIT'].max(), num=3))) #for PHIE phi = ax[6].twiny() phi.plot(data['PHIE'], data[self._depth], color='red', linewidth=0.5) phi.fill_betweenx(data[self._depth], data["PHIE"], data['PHIE'].max(), color='lightblue') phi.set_xlim(data['PHIE'].min(), data['PHIE'].max()) phi.set_ylim(data[self._depth].min(), data[self._depth].max()) phi.invert_yaxis() phi.invert_xaxis() phi.xaxis.label.set_color('red') phi.tick_params(axis='x', colors='red') phi.spines['top'].set_edgecolor('red') phi.set_xlabel('PHIE', color='red') phi.spines["top"].set_position(("axes", 1.05)) phi.xaxis.set_ticks_position("top") phi.xaxis.set_label_position("top") phi.set_xticks(list(np.linspace(data['PHIE'].min(), data['PHIE'].max(), num=3))) #formation subplot ax[7].set_ylim(data[self._depth].min(), data[self._depth].max()); ax[7].invert_yaxis() ax[7].set_title('Zones', pad=45) ax[7].set_xticks([]) ax[7].set_yticklabels([]) ax[7].set_xticklabels([]) ax[7].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) ax[7].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid', linewidth=1.) formations = ax[7] # #delineating zones cycol = cycle('bgyrcmk') color = [choice(next(cycol)) for i in range(len(self._zonename))] np.random.shuffle(color) for i in ax: for t,b, c in zip(self._ztop, self._zbot, color): i.axhspan(t, b, color=c, alpha=0.2) # pass #adding zone names for label, fm_mids in zip(self._zonename, self._f_mids): formations.text(0.5, fm_mids, label, rotation=0, verticalalignment='center', fontweight='bold', fontsize='large') #for flags im=ax[8].imshow(cluster1, interpolation='none', aspect='auto', cmap=cmap_facies,vmin=0,vmax=1) im=ax[9].imshow(cluster2, interpolation='none', aspect='auto', cmap=cmap_facies,vmin=0,vmax=1) im=ax[10].imshow(cluster3, interpolation='none', aspect='auto', cmap=cmap_facies,vmin=0,vmax=1) divider = make_axes_locatable(ax[10]) cax = divider.append_axes("right", size="20%", pad=0.05) cbar = plt.colorbar(im, cax=cax) cbar.set_label((17*' ').join([ 'Gross Flag', 'Net Flag' ])) cbar.set_ticks(range(0,1)); cbar.set_ticklabels('') x = [8, 9, 10] flag_name = ['ROCK', 'RES', 'PAY'] for i, f in zip(x, flag_name): ax[i].set_title(f, pad=45) ax[i].set_xticks([]) ax[i].set_yticklabels([]) ax[i].set_xticklabels([]) # ax[i].hlines([t for t in self._ztop], xmin=0, xmax=1, colors='black', linestyles='solid') # ax[i].hlines([b for b in self._zbot], xmin=0, xmax=1, colors='black', linestyles='solid') plt.tight_layout(h_pad=1) fig.subplots_adjust(wspace = 0.01) return new_data elif show_plot==False: # assert figsize == None, f"show_plot is set to {show_plot}. Set figsize as None" return new_data
[docs] def paySummary(self, name:str): ''' Computes the * net, grossand not net thicknesses * net-to-gross * average volume of shale * average porosity value * bulk volume of water * water saturation for each of the three flags {ROCK, RES, PAY} Parameters ---------- name: str Name of the well Returns ------- Displays the Pay Summary Report table Example ------- >>> pp.paySummary(name='15-9_F1A') ''' self._name = name new_data = self.flags(self._por_cutoff, self._vsh_cutoff, self._sw_cutoff, show_plot=False, palette_op=self._pale) # attributes flag_name= list() net = list() gross = list() top = list() bot = list() unit = list() zone_name = list() net_to_gross = list() wellname = list() avg_bvw = list() # por_th = list() # hcpor_th = list() avg_shale = list() avg_por = list() avg_sw = list() not_net = list() # np.random.seed(2) for d, i in zip(new_data, self._zonename): # np.random.seed(2) # top and bottom and unit top_ = d[self._depth].min() bot_ = d[self._depth].max() top.append([top_, top_, top_]) bot.append([bot_, bot_, bot_]) unit.append([self._ref, self._ref, self._ref]) #ntg ntg_rock = d['ROCK_NET_FLAG'].sum()/d.shape[0] ntg_res = d['RES_NET_FLAG'].sum()/d.shape[0] ntg_pay = d['PAY_NET_FLAG'].sum()/d.shape[0] net_to_gross.append([ntg_rock, ntg_res, ntg_pay]) #gross gross_ = d[self._depth].max() - d[self._depth].min() gross.append([gross_, gross_, gross_]) #net net_rock = ntg_rock * gross_ net_res = ntg_res * gross_ net_pay = ntg_pay * gross_ net.append([net_rock, net_res, net_pay]) #not net not_net.append([gross_-net_rock, gross_-net_res, gross_-net_pay]) #zonename and flag name flag_name.append(['ROCK', 'RES', 'PAY']) zone_name.append([i, i, i]) #average volume of shale, water saturationa and porosity rock_filter = d[d['ROCK_NET_FLAG'] == 1] res_filter = d[d['RES_NET_FLAG'] == 1] pay_filter = d[d['PAY_NET_FLAG'] == 1] #avg shale avg_shale_rock = rock_filter['VShale'].mean() avg_shale_res = res_filter['VShale'].mean() avg_shale_pay = pay_filter['VShale'].mean() avg_shale.append([avg_shale_rock, avg_shale_res, avg_shale_pay]) #avg porosity avg_por_rock = rock_filter['PHIE'].mean() avg_por_res = res_filter['PHIE'].mean() avg_por_pay = pay_filter['PHIE'].mean() avg_por.append([avg_por_rock, avg_por_res, avg_por_pay]) #avg water saturation avg_water_rock = rock_filter['SW'].mean() avg_water_res = res_filter['SW'].mean() avg_water_pay = pay_filter['SW'].mean() avg_sw.append([avg_water_rock, avg_water_res, avg_water_pay]) #avg bulk volume of water avg_bulk_rock = (rock_filter['SW'] * rock_filter['PHIE']).mean() avg_bulk_res = (res_filter['SW'] * res_filter['PHIE']).mean() avg_bulk_pay = (pay_filter['SW'] * pay_filter['PHIE']).mean() avg_bvw.append([avg_bulk_rock, avg_bulk_res, avg_bulk_pay]) #wellname wellname.append([name, name, name]) #store info in dataframe df = { 'Well': wellname, 'Zones' : zone_name, 'Flag Name': flag_name, 'Top': top, 'Bottom': bot, 'Unit': unit, 'Gross': gross, 'Net': net, 'Not Net': not_net, 'NTG': net_to_gross, 'BVW': avg_bvw, 'Average VShale': avg_shale, 'Average Porosity': avg_por, 'Average Water Saturation': avg_sw } summary = pd.DataFrame(df).apply(pd.Series.explode).reset_index(drop=True) n = len(summary.columns) def highlight(x): ''' highlighting cell colors by flag type ''' if x['Flag Name']== 'ROCK': return ["background-color: yellow"]*n elif x['Flag Name'] == 'RES': return ["background-color: green"]*n else: return ["background-color: red"]*n # ref https://stackoverflow.com/questions/57958432/how-to-add-table-title-in-python-preferably-with-pandas self._styles = [dict(selector="caption", props=[("text-align", "center"), ("font-size", "150%"), ("color", 'black')])] summary = summary.style.apply(highlight, axis = 1).set_caption(f"Workflow Table Result MD for {name.upper()}").set_table_styles(self._styles) return summary
[docs] def report(self): ''' Displays the methods used in each parameter estimations and cutoff used for flagging ''' df = { 'VShale': self._v_method.title(), 'Porosity': self._p_method.title(), 'Water Saturation': self._sw_method.title(), 'VSH Cutoff':self._vsh_cutoff, 'SH Cutoff': self._sw_cutoff, 'PHI Cutoff': self._por_cutoff } df = pd.DataFrame.from_records(df, index=np.arange(0, 1)).T.rename({0:''}, axis=1).style.set_caption('Methods and Cutoffs').set_table_styles(self._styles) return df
[docs] def save(self, file_name:str): ''' A method to save the pay summary results into a excel file Parameters ---------- file_name : str name of to save the pay summary report as ''' c = self.paySummary(self._name) c.to_excel(file_name+'.xlsx', index=False)