From e6d0b8c56bc80a2bcf5eca5cba50e4f0f826813a Mon Sep 17 00:00:00 2001 From: AlexanderJuestel Date: Mon, 1 Apr 2024 21:29:19 +0200 Subject: [PATCH] EditFiles --- pyborehole/borehole.py | 24 +- pyborehole/logs.py | 2450 --------------------------------------- pyborehole/logs_dlis.py | 1058 +++++++++++++++++ pyborehole/logs_las.py | 1125 ++++++++++++++++++ pyborehole/logs_lis.py | 232 ++++ pyborehole/plot.py | 611 ++++++++++ 6 files changed, 3044 insertions(+), 2456 deletions(-) create mode 100644 pyborehole/logs_dlis.py create mode 100644 pyborehole/logs_las.py create mode 100644 pyborehole/logs_lis.py create mode 100644 pyborehole/plot.py diff --git a/pyborehole/borehole.py b/pyborehole/borehole.py index 65ff0e5..d38ce52 100644 --- a/pyborehole/borehole.py +++ b/pyborehole/borehole.py @@ -7,7 +7,9 @@ from pyborehole.deviation import Deviation from pyborehole.design import WellDesign -from pyborehole.logs import LASLogs, DLISLogs +from pyborehole.logs_las import LASLogs +from pyborehole.logs_dlis import DLISLogs +from pyborehole.logs_lis import LISLogs from pyborehole.litholog import LithoLog from pyborehole.tops import WellTops @@ -89,7 +91,7 @@ class Borehole: deviation : Deviation, default: ``None`` Deviation Object. - logs : Union[LASLogs, DLISLogs], default: ``None`` + logs : Union[LASLogs, DLISLogs, LISLogs], default: ``None`` Well Log Object. well_tops : Well Tops, default: ``None`` Well Tops Object. @@ -301,7 +303,7 @@ def __init__(self, deviation : Deviation, default: ``None`` Deviation Object. - logs : Union[LASLogs, DLISLogs], default: ``None`` + logs : Union[LASLogs, DLISLogs, LISLogs], default: ``None`` Well Log Object. well_tops : Well Tops, default: ``None`` Well Tops Object. @@ -1537,7 +1539,7 @@ def add_deviation(self, def add_well_logs(self, path: str, - nodata: Union[int, float] = -9999) -> Union[LASLogs, DLISLogs]: + nodata: Union[int, float] = -9999) -> Union[LASLogs, DLISLogs, LISLogs]: """Add Well Logs to the Borehole Object. Parameters @@ -1549,7 +1551,7 @@ def add_well_logs(self, Returns _______ - LASLogs or DLISLogs + LASLogs, DLISLogs, or LISLogs Well Logs Object. Raises @@ -1621,8 +1623,16 @@ def add_well_logs(self, self.logs = DLISLogs(self, path=path, nodata=nodata) + + # Opening LIS file if provided + elif path.endswith('.lis') or path.endswith('.LIS'): + # Creating well logs from LIS file + self.logs = LISLogs(self, + path=path, + nodata=nodata) + else: - raise ValueError('Please provide a LAS file or DLIS file') + raise ValueError('Please provide a LAS, DLIS or LIS file') # Setting attributes self.has_logs = True @@ -1716,6 +1726,8 @@ def add_well_tops(self, # Creating well tops self.well_tops = WellTops(path=path, delimiter=delimiter, + top_column=top_column, + depth_column=depth_column, unit=unit) # Checking that the top column is in the DataFrame diff --git a/pyborehole/logs.py b/pyborehole/logs.py index ebc117e..edc0f1a 100644 --- a/pyborehole/logs.py +++ b/pyborehole/logs.py @@ -10,2456 +10,6 @@ import copy -class LASLogs: - """Class to initiate a Well Log Object. - - Parameters - __________ - borehole : Borehole - Borehole object. - path : str - Path to the well logs, e.g. ``path='logs.las'``. - - Attributes - __________ - df : pd.DataFrame - Pandas DataFrame containing the values of all curves. Data columns are as follows: - - ======= ===================================== - Index Index of each measurment - DEPTH Measured depth of each measurment - Curves Columns containing the measurements - ======= ===================================== - - **Example:** - - ======= ======= ======== ======= ======= ======== - Index DEPTH SGR TH U K - ======= ======= ======== ======= ======= ======== - 0 0.05 9.6514 1.8866 0.4018 0.2414 - 1 0.10 10.5465 2.1613 0.4206 0.2562 - 2 0.15 11.4666 2.4533 0.4397 0.2698 - 3 0.20 12.3323 2.7352 0.4601 0.2800 - ======= ======= ======== ======= ======= ======== - - curves : pd.DataFrame - Pandas DataFrame providing information about the measured curves. Data columns are as follows: - - =================== ============================================== - Index Index of each curve - original_mnemonic Origial name of a curve in the well log file - mnemonic Name of a curve in the well log file - descr Description of a curve in the well log file - unit Unit of a curve in the well log file - =================== ============================================== - - **Example:** - - ====== ================== ========= ======================================= ===== - Index original_mnemonic mnemonic descr unit - 0 DEPT DEPT Borehole Depth M - 1 SGR SGR Gamma Ray derived from Spectrum gAPI - 2 TH TH Thorium content from Spectrum Gamma ppm - 3 U U Uranium content from Spectrum Gamma ppm - 4 K K Potassium content from Spectrum Gamma %wt - ====== ================== ========= ======================================= ===== - - well_header : pd.DataFrame - Pandas DataFrame providing information about the well. Data columns are as follows: - - ========== ===================================== - Index Index of each well information - mmnemonic Name of the well information - unit Unit of the well information - value Value of the well information - descr Description of the well information - ========== ===================================== - - **Example:** - - ====== ========= ===== =============== =================== - Index mnemonic unit value descr - ====== ========= ===== =============== =================== - 0 STRT M 100.0 Log Start Depth - 1 STOP M 0.05 Log Stop Depth - 2 STEP M -0.05 Log Increment - 3 NULL -999.25 Null Value - 4 COMP RWE Power Company Name - 5 WELL EB 1 Well Name - 6 FLD KW Weisweiler Field Name - 7 LOC Location - 8 PROV Province - 9 SRVC Service Company - 10 DATE 26-Oct-2023 Date - 11 UWI Unique Well ID - ====== ========= ===== =============== =================== - - params : pd.DataFrame - Pandas DataFrame containing information about the well. Data columns are as follows: - - ========== ===================================== - Index Index of each well information - mmnemonic Name of the well information - unit Unit of the well information - value Value of the well information - descr Description of the well information - ========== ===================================== - - **Example:** - - ======= ========== ================ ================= - Index mnemonic unit value descr - ======= ========== ================ ================= - 0 BHT 20 degC BHT - 1 CVL 5350 m/s CasingVelocity - 2 CTRY NRW Country - 3 FLVT 1500 m/s FluidVelocity - 4 LMFI 0.6 G MFieldIntensity - 5 MUDD 1.0 g/ccm MudWeight - 6 RECORDED Mustermann Engineer's Name - 7 RUN 1 Run Number - 8 TIME 20:59:35 Time - 9 WITTNESS Herr Mustermann Witness's Name - ======= ========== ================ ================= - - - Returns - _______ - LASLogs - Well Log Object. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.well_header - - ======== ========= ====== ================ =================== - Index mnemonic unit value descr - ======== ========= ====== ================ =================== - 0 STRT M 100.0 Log Start Depth - 1 STOP M 0.05 Log Stop Depth - 2 STEP M -0.05 Log Increment - 3 NULL -999.25 Null Value - 4 COMP RWE Power Company Name - 5 WELL EB 1 Well Name - 6 FLD KW Weisweiler Field Name - 7 LOC Location - 8 PROV Province - 9 SRVC Service Company - 10 DATE 26-Oct-2023 Date - 11 UWI Unique Well ID - ======== ========= ====== ================ =================== - - See Also - ________ - pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. - pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. - pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. - pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. - - .. versionadded:: 0.0.1 - """ - - def __init__(self, - borehole, - path: str): - """Class to initiate a Well Log Object. - - Parameters - __________ - borehole : Borehole - Borehole object. - path : str - Path to the well logs, e.g. ``path='logs.las'``. - - Attributes - __________ - df : pd.DataFrame - Pandas DataFrame containing the values of all curves. Data columns are as follows: - - ======= ===================================== - Index Index of each measurment - DEPTH Measured depth of each measurment - Curves Columns containing the measurements - ======= ===================================== - - **Example:** - - ======= ======= ======== ======= ======= ======== - Index DEPTH SGR TH U K - ======= ======= ======== ======= ======= ======== - 0 0.05 9.6514 1.8866 0.4018 0.2414 - 1 0.10 10.5465 2.1613 0.4206 0.2562 - 2 0.15 11.4666 2.4533 0.4397 0.2698 - 3 0.20 12.3323 2.7352 0.4601 0.2800 - ======= ======= ======== ======= ======= ======== - - curves : pd.DataFrame - Pandas DataFrame providing information about the measured curves. Data columns are as follows: - - =================== ============================================== - Index Index of each curve - original_mnemonic Origial name of a curve in the well log file - mnemonic Name of a curve in the well log file - descr Description of a curve in the well log file - unit Unit of a curve in the well log file - =================== ============================================== - - **Example:** - - ====== ================== ========= ======================================= ===== - Index original_mnemonic mnemonic descr unit - 0 DEPT DEPT Borehole Depth M - 1 SGR SGR Gamma Ray derived from Spectrum gAPI - 2 TH TH Thorium content from Spectrum Gamma ppm - 3 U U Uranium content from Spectrum Gamma ppm - 4 K K Potassium content from Spectrum Gamma %wt - ====== ================== ========= ======================================= ===== - - well_header : pd.DataFrame - Pandas DataFrame providing information about the well. Data columns are as follows: - - ========== ===================================== - Index Index of each well information - mmnemonic Name of the well information - unit Unit of the well information - value Value of the well information - descr Description of the well information - ========== ===================================== - - **Example:** - - ====== ========= ===== =============== =================== - Index mnemonic unit value descr - ====== ========= ===== =============== =================== - 0 STRT M 100.0 Log Start Depth - 1 STOP M 0.05 Log Stop Depth - 2 STEP M -0.05 Log Increment - 3 NULL -999.25 Null Value - 4 COMP RWE Power Company Name - 5 WELL EB 1 Well Name - 6 FLD KW Weisweiler Field Name - 7 LOC Location - 8 PROV Province - 9 SRVC Service Company - 10 DATE 26-Oct-2023 Date - 11 UWI Unique Well ID - ====== ========= ===== =============== =================== - - params : pd.DataFrame - Pandas DataFrame containing information about the well. Data columns are as follows: - - ========== ===================================== - Index Index of each well information - mmnemonic Name of the well information - unit Unit of the well information - value Value of the well information - descr Description of the well information - ========== ===================================== - - **Example:** - - ======= ========== ================ ================= - Index mnemonic unit value descr - ======= ========== ================ ================= - 0 BHT 20 degC BHT - 1 CVL 5350 m/s CasingVelocity - 2 CTRY NRW Country - 3 FLVT 1500 m/s FluidVelocity - 4 LMFI 0.6 G MFieldIntensity - 5 MUDD 1.0 g/ccm MudWeight - 6 RECORDED Mustermann Engineer's Name - 7 RUN 1 Run Number - 8 TIME 20:59:35 Time - 9 WITTNESS Herr Mustermann Witness's Name - ======= ========== ================ ================= - - Returns - _______ - LASLogs - Well Log Object. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.well_header - - ======== ========= ====== ================ =================== - Index mnemonic unit value descr - ======== ========= ====== ================ =================== - 0 STRT M 100.0 Log Start Depth - 1 STOP M 0.05 Log Stop Depth - 2 STEP M -0.05 Log Increment - 3 NULL -999.25 Null Value - 4 COMP RWE Power Company Name - 5 WELL EB 1 Well Name - 6 FLD KW Weisweiler Field Name - 7 LOC Location - 8 PROV Province - 9 SRVC Service Company - 10 DATE 26-Oct-2023 Date - 11 UWI Unique Well ID - ======== ========= ====== ================ =================== - - .. versionadded:: 0.0.1 - """ - # Importing lasio - try: - import lasio - except ModuleNotFoundError: - ModuleNotFoundError('lasio package not installed') - - # Checking that the path is provided as string - if not isinstance(path, str): - raise TypeError('The path must be provided as string') - - # Opening LAS file - las = lasio.read(path) - - # Extracting DataFrame from LAS file - self.df = las.df().sort_index() - - # Changing the name of the index column and reset index - self.df.index.rename('DEPTH', inplace=True) - self.df = self.df.reset_index() - - # Creating DataFrame from curve data - self.curves = pd.DataFrame(list(zip([las.curves[i]['original_mnemonic'] for i in range(len(las.curves))], - [las.curves[i]['mnemonic'] for i in range(len(las.curves))], - [las.curves[i]['descr'] for i in range(len(las.curves))], - [las.curves[i]['unit'] for i in range(len(las.curves))])), - columns=['original_mnemonic', - 'mnemonic', - 'descr', - 'unit']) - - # Creating DataFrame from well header - self.well_header = pd.DataFrame(list(zip([las.well[i]['mnemonic'] for i in range(len(las.well))], - [las.well[i]['unit'] for i in range(len(las.well))], - [las.well[i]['value'] for i in range(len(las.well))], - [las.well[i]['descr'] for i in range(len(las.well))])), - columns=['mnemonic', - 'unit', - 'value', - 'descr']) - - # Creating DataFrame from parameters - self.params = pd.DataFrame(list(zip([las.params[i]['mnemonic'] for i in range(len(las.params))], - [las.params[i]['unit'] for i in range(len(las.params))], - [las.params[i]['value'] for i in range(len(las.params))], - [las.params[i]['descr'] for i in range(len(las.params))])), - columns=['mnemonic', - 'unit', - 'value', - 'descr']) - - # Assigning attributes - self.well_tops = borehole.well_tops - self.has_well_tops = borehole.has_well_tops - self.has_well_design = borehole.has_well_design - self.well_design = borehole.well_design - self._borehole = borehole - - def plot_well_logs(self, - tracks: Union[str, list], - depth_column: str = 'DEPTH', - depth_min: Union[int, float] = None, - depth_max: Union[int, float] = None, - colors: Union[str, list] = None, - add_well_tops: bool = False, - add_well_design: bool = False, - fill_between: int = None, - add_net_to_gross: int = None) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: - """Plot well logs. - - Parameters - __________ - - tracks : Union[str, list] - Name/s of the logs to be plotted, e.g. ``tracks='SGR'`` or ``tracks=['SGR', 'K']``. - depth_column : str, default: ``'DEPTH'`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - depth_min : Union[int, float] - Minimum depth to be plotted, e.g. ``depth_min=0``. - depth_max : Union[int, float] - Maximum depth to be plotted, e.g. ``depth_max=2000``. - colors : Union[str, list], default: ``None`` - Colors of the logs, e.g. ``colors='black'`` or ``colors=['black', 'blue']``. - add_well_tops : bool, default: ``False`` - Boolean to add well tops to the plot, e.g. ``add_well_tops=True``. - add_well_design : bool, default: ``False`` - Boolean to add well design to the plot, e.g. ``add_well_design=True``. - fill_between : int, default: ``None`` - Number of the axis to fill, e.g. ``fill_between=0``. - add_net_to_gross : int, default: ``None`` - Number of axis to fill with the net to gross values, e.g. ``add_net_to_gross=0``. - - Returns - _______ - fig : matplotlib.figure.Figure - Matplotlib Figure. - ax : matplotlib.axes.Axes - Matplotlib Axes. - - Raises - ______ - TypeError - If the wrong input data types are provided. - ValueError - If no well tops are provided but ``add_well_tops`` is set to ``True``. - ValueError - If the wrong column names are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.plot_well_logs(tracks=['SGR', 'CAL'], depth_column='DEPTH', colors=['black', 'red']) - - See Also - ________ - plot_well_log_along_path : Plot well log along path. - calculate_vshale : Calculate Shale Volume. - calculate_vshale_linear : Calculate Shale Volume linear method. - calculate_net_to_gross : Calculate net to gross. - - .. versionadded:: 0.0.1 - """ - # Checking that the tracks are provided as list or string - if not isinstance(tracks, (list, str)): - raise TypeError('The track/s must bei either provided as str or a list of strings') - - # Checking that the tracks are in the DataFrame - if isinstance(tracks, str): - if not {tracks}.issubset(self.df.columns): - raise ValueError('The track is not part of the curves') - elif isinstance(tracks, list): - if not all({track}.issubset(self.df.columns) for track in tracks): - raise ValueError('Not all tracks are part of the curves') - - # Checking that the depth column is of type string - if not isinstance(depth_column, str): - raise TypeError('Depth_column must be provided as string') - - # Checking that the depth column is in the DataFrame - if not {depth_column}.issubset(self.df): - raise ValueError('The depth_column is not part of the curves') - - # Checking that depth_min is of type float or int - if not isinstance(depth_min, (int, float, type(None))): - raise ValueError('The minimum depth must be provided as int or float') - - # Checking that depth_max is of type float or int - if not isinstance(depth_max, (int, float, type(None))): - raise ValueError('The maximum depth must be provided as int or float') - - # Checking that the colors are provided as list or string - if not isinstance(colors, (list, str, type(None))): - raise TypeError('The track/s must bei either provided as str or a list of strings') - - # Checking that the add_well_tops variable is of type bool - if not isinstance(add_well_tops, bool): - raise TypeError('The add_well_tops variable must be provided as bool') - - # Checking that the add_well_design variable is of type bool - if not isinstance(add_well_design, bool): - raise TypeError('The add_well_design variable must be provided as bool') - - # Checking that the fill_between variable is of type int - if not isinstance(fill_between, (int, type(None))): - raise TypeError('The fill_between variable must be provided as int') - - # Checking that the add_net to_gross variable is of type bool - if not isinstance(add_net_to_gross, (int, type(None))): - raise TypeError('The add_net_to_gross variable must be provided as int') - - if isinstance(tracks, str): - tracks_str = tracks - tracks = [tracks] - - # Selecting tracks - df = self.df[tracks + [depth_column]].reset_index() - - # Converting list back to string - if len(tracks) == 1: - tracks = tracks_str - - # Creating plot if tracks is of type string - if isinstance(tracks, str): - - # Creating plot - fig, ax = plt.subplots(1, 1, figsize=(1 * 2, 8)) - - ax.plot(df[tracks], df[depth_column], color=colors) - ax.grid() - ax.invert_yaxis() - if not depth_min: - depth_min = min(df[depth_column]) - if not depth_max: - depth_max = max(df[depth_column]) - - buffer = (depth_max - depth_min) / 20 - - ax.set_ylim(depth_max + buffer, depth_min - buffer) - ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) - ax.xaxis.set_label_position('top') - ax.set_xlabel(tracks + ' [%s]' % - self.curves[self.curves['original_mnemonic'] == tracks].reset_index(drop=True)['unit'].iloc[ - 0], color='black') - ax.set_ylabel(depth_column + ' [m]') - - # Fill between curve - if fill_between is not None: - left_col_value = np.min(df[tracks].dropna().values) - right_col_value = np.max(df[tracks].dropna().values) - span = abs(left_col_value - right_col_value) - cmap = plt.get_cmap('hot_r') - color_index = np.arange(left_col_value, right_col_value, span / 100) - # loop through each value in the color_index - for index in sorted(color_index): - index_value = (index - left_col_value) / span - color = cmap(index_value) # obtain color for color index value - ax.fill_betweenx(df[depth_column], df[tracks], left_col_value, where=df[tracks] >= index, - color=color) - - return fig, ax - - # Creating plot if tracks is of type list - elif isinstance(tracks, list): - - # Helping variable for adding well tops - if add_well_tops: - j = 1 - else: - j = 0 - - if add_well_design: - k = 1 - else: - k = 0 - - # Setting colors - if not colors: - colors = [None] * len(tracks) - - # Creating plot - fig, ax = plt.subplots(1, - len(tracks) + j + k, - figsize=((len(tracks) + j + k) * 1.8, 8), - sharey=True) - - # Adding well tops - if add_well_tops: - if not self.has_well_tops: - raise ValueError('Please add well tops to the borehole object to plot them') - else: - for index, row in self.well_tops.df.iterrows(): - ax[0 + k].axhline(row[self.well_tops.df.columns[1]], 0, 1, color='black') - ax[0 + k].text(0.05, row[self.well_tops.df.columns[1]] - max(self.df[depth_column]) / 150, - s=row[self.well_tops.df.columns[0]], - fontsize=max(df[depth_column]) / 75) - ax[0 + k].grid() - ax[0 + k].xaxis.set_label_position('top') - ax[0 + k].set_xlabel('Well Tops') - ax[0 + k].axes.get_xaxis().set_ticks([]) - - # Adding well design - if add_well_design: - if not self.has_well_design: - raise ValueError('Please add a well design to the borehole object to plot it') - else: - for key, elem in self.well_design.pipes.items(): - if elem.pipe_type == 'open hole section': - # Plotting open hole section as wavy line - y = np.linspace(elem.top, elem.bottom, 1000) - x1 = 0.5 * np.sin(y / 2) - elem.inner_diameter + 0.5 - x2 = 0.5 * np.cos(y / 2 + np.pi / 4) + elem.inner_diameter - ax[k].plot(x1, y, color='black') - ax[k].plot(x2, y, color='black') - # Plotting open hole section as dashed line - # ax.plot([elem.inner_diameter,elem.inner_diameter], - # [elem.top, elem.bottom], - # linestyle='--', - # color='black') - # ax.plot([-elem.inner_diameter, -elem.inner_diameter], - # [elem.top, elem.bottom], - # linestyle='--', - # color='black') - else: - ax[k].add_patch(Rectangle(elem.xy, elem.width, elem.height, color="black")) - ax[k].add_patch( - Rectangle((-1 * elem.xy[0], elem.xy[1]), -1 * elem.width, elem.height, color="black")) - - # Deep copy of pipes dict - x = copy.deepcopy(self.pipes) - - # Popping open hole section - try: - type_list = [elem.pipe_type for key, elem in x.items()] - index_open_hole = type_list.index('open hole section') - key_open_hole = list(x.keys())[index_open_hole] - x.pop(key_open_hole) - except ValueError: - pass - - # Getting diameters and calculating thickness of cement between each pipe - outer_diameters = sorted([elem.outer_diameter for key, elem in x.items()], reverse=False) - inner_diameters = sorted([elem.inner_diameter for key, elem in x.items()], reverse=False) - thicknesses = [y - x for x, y in zip(outer_diameters[:-1], inner_diameters[1:])] - - # Sorting pipes - pipes_sorted = {k: v for k, v in sorted(x.items(), key=lambda item: item[1].outer_diameter)} - - # Selecting pies - pipes_selected = {k: pipes_sorted[k] for k in list(pipes_sorted)[:len(thicknesses)]} - - # Plotting top of pipe - i = 0 - for key, elem in pipes_selected.items(): - i = i - ax[k].add_patch( - Rectangle((elem.inner_diameter, elem.top), thicknesses[i] + elem.thickness, 1, - color="black")) - ax[k].add_patch(Rectangle((-1 * elem.inner_diameter, elem.top), - -1 * thicknesses[0] - elem.thickness, 1, - color="black")) - i = i + 1 - - # Plotting Casing Shoes - for key, elem in self.well_design.pipes.items(): - if elem.shoe_width is not None: - p0 = [elem.outer_diameter, elem.bottom] - p1 = [elem.outer_diameter, elem.bottom - elem.shoe_height] - p2 = [elem.outer_diameter + elem.shoe_width, elem.bottom] - shoe = plt.Polygon([p0, p1, p2], color="black") - ax.add_patch(shoe) - p0[0] *= -1 - p1[0] *= -1 - p2 = [-elem.outer_diameter - elem.shoe_width, elem.bottom] - shoe = plt.Polygon([p0, p1, p2], color="black") - ax[k].add_patch(shoe) - - # Adding Casing Cements - for key, elem in self.well_design.cements.items(): - ax[k].fill_between(elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) - ax[k].fill_between(-1 * elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) - - # Calculating axes limits - top = np.min([elem.top for key, elem in self.well_design.pipes.items()]) - bottom = np.max([elem.bottom for key, elem in self.well_design.pipes.items()]) - max_diam = np.max([elem.outer_diameter for key, elem in self.well_design.pipes.items()]) - - # Setting axes limits - if not depth_min: - depth_min = min(df[depth_column]) - if not depth_max: - depth_max = max(df[depth_column]) - - buffer = (depth_max - depth_min) / 20 - - ax[k].set_ylim(depth_max + buffer, depth_min - buffer) - ax.set_xlim(-max_diam * 1, max_diam * 1) - # ax.invert_yaxis() - - # Setting axes labels - ax.set_xlabel('Diameter [in]') - - # Plotting tracks - for i in range(len(tracks)): - ax[i + j + k].plot(df[tracks[i]], df[depth_column], color=colors[i]) - ax[i + j + k].grid() - ax[i + j + k].invert_yaxis() - buffer = (max(df[depth_column]) - min(df[depth_column])) / 20 - ax[i + j + k].set_ylim(max(df[depth_column]) + buffer, min(df[depth_column]) - buffer) - ax[i + j + k].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) - ax[i + j + k].xaxis.set_label_position('top') - ax[i + j + k].set_xlabel(tracks[i] + ' [%s]' % - self.curves[self.curves['original_mnemonic'] == tracks[i]].reset_index( - drop=True)[ - 'unit'].iloc[0], - color='black' if isinstance(colors[i], type(None)) else colors[i]) - ax[0].set_ylabel(depth_column + ' [m]') - - if not depth_min: - depth_min = min(df[depth_column]) - if not depth_max: - depth_max = max(df[depth_column]) - - buffer = (depth_max - depth_min) / 20 - - ax[0].set_ylim(depth_max + buffer, depth_min - buffer) - - # Fill between curves - if fill_between is not None: - left_col_value = np.min(df[tracks[fill_between]].dropna().values) - right_col_value = np.max(df[tracks[fill_between]].dropna().values) - span = abs(left_col_value - right_col_value) - cmap = plt.get_cmap('hot_r') - color_index = np.arange(left_col_value, right_col_value, span / 100) - - # Dropping duplicate columns if the same track is selected twice - try: - df1 = df[tracks[fill_between]].copy(deep=True) - df1 = df1.loc[:, ~df1.columns.duplicated()] - except AttributeError: - df1 = df[tracks[fill_between]].copy(deep=True) - - # loop through each value in the color_index - for index in sorted(color_index): - index_value = (index - left_col_value) / span - color = cmap(index_value) # obtain color for color index value - ax[fill_between + j].fill_betweenx(df[depth_column], - # - df1.values.flatten(), - left_col_value, - where=df1.values.flatten() >= index, - color=color) - - # Adding net to gross - if add_net_to_gross is not None: - if 'N/G' not in self.df.columns: - raise ValueError('Net to gross has not been calculated yet') - - ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], - self.df[tracks[add_net_to_gross]], - 0, - where=self.df['N/G'] == 1, - color='yellow') - ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], - self.df[tracks[add_net_to_gross]], - 0, - where=self.df['N/G'] == 0, - color='brown') - - plt.tight_layout() - - return fig, ax - - def plot_well_log_along_path(self, - log: str, - depth_column: str = 'DEPTH', - relative: bool = True, - spacing: Union[float, int] = 0.5, - radius_factor: Union[float, int] = 75): - """Plot well log along path. - - Parameters - __________ - log : str - Name of the log, e.g. ``log='GR'``. - depth_column : str, default: ``'DEPTH'`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - relative : bool, default: ``True`` - Boolean value to plot the tube with relative coordinates, e.g. ``relative=False``. - spacing : Union[float, int], default: ``0.5`` - Spacing for the resampling of the well path, e.g. ``spacing=0.5``. - radius_factor : Union[float, int], default: ``75`` - Factor to scale the radius of the tube, e.g. ``radius_factor=75``. - - Return - ______ - tube : pv.core.pointset.PolyData - PyVista Tube of the borehole. - - Raises - ______ - ModuleNotFoundError - Raises error if PyVista is not installed. - TypeError - If the wrong input data types are provided. - ValueError - Raises error if the wrong column names are provided. - ValueError - Raises error if no ``Deviation`` has been defined. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.plot_well_log_along_path(log='SGR', depth_column='DEPTH', spacing=0.5, radius_factor=10) - - =========== ========================= - PolyData Information - =========== ========================= - N Cells 22 - N Points 40040 - N Strips 22 - X Bounds 3.413e+06, 3.413e+06 - Y Bounds 5.836e+06, 5.836e+06 - Z Bounds -9.925e+01, -5.000e-02 - N Arrays 2 - =========== ========================= - - ============ ======= ======== ======= =========== ========== - Name Field Type N Comp Min Max - ============ ======= ======== ======= =========== ========== - values Points float64 1 4.342e+00 1.474e+02 - TubeNormals Points float32 3 -1.000e+00 1.000e+00 - ============ ======= ======== ======= =========== ========== - - See Also - ________ - plot_well_logs : Plot well logs. - - .. versionadded:: 0.0.1 - """ - # Importing pyvista - try: - import pyvista as pv - except ModuleNotFoundError: - ModuleNotFoundError('PyVista package not installed') - - # Checking that log is provided as string - if not isinstance(log, str): - raise TypeError('The name of the log must be provided as str') - - # Checking that the depth column is of type string - if not isinstance(depth_column, str): - raise TypeError('Depth_column must be provided as string') - - # Checking that the relative value is provided as bool - if not isinstance(relative, bool): - raise TypeError('The relative value must be provided as bool') - - # Checking that the spacing is of type float or int - if not isinstance(spacing, (float, int)): - raise TypeError('The spacing for the resampling must be provided as float or int') - - # Checking that the radius_factor is of type float or int - if not isinstance(radius_factor, (float, int)): - raise TypeError('The radius_factor must be provided as float or int') - - # Checking if the borehole deviation is present - if not self._borehole.deviation: - raise ValueError('Borehole deviation is needed to create a tube') - - # Extracting the coordinates - coordinates = self._borehole.deviation.desurveyed_df.copy(deep=True) - - if relative: - if not {'Northing_rel', 'Easting_rel', 'True Vertical Depth'}.issubset(coordinates.columns): - raise ValueError( - 'The coordinates DataFrame must contain Northing_rel, Easting_rel and True Vertical Depth ' - 'below sea level column') - - coordinates['True Vertical Depth'] = coordinates['True Vertical Depth'] * (-1) - xyz = coordinates[['Easting_rel', 'Northing_rel', 'True Vertical Depth']].to_numpy() - - else: - if not {'Northing', 'Easting', 'True Vertical Depth Below Sea Level'}.issubset(coordinates.columns): - raise ValueError( - 'The coordinates DataFrame must contain Northing, Easting and True Vertical Depth Below Sea Level ' - 'below sea level column') - - coordinates['True Vertical Depth Below Sea Level'] = coordinates['True Vertical Depth Below Sea Level'] * ( - -1) - xyz = coordinates[['Easting', 'Northing', 'True Vertical Depth Below Sea Level']].to_numpy() - - # Obtaining log values - logs = self.df.reset_index()[[depth_column, log]] - - # Resample points - points = resample_between_well_deviation_points(coordinates=xyz, - spacing=spacing) - - # Creating spline - polyline_well_path_resampled = pv.Spline(points) - - # Getting points along the spline - points_along_spline = get_points_along_spline(spline=polyline_well_path_resampled, - dist=logs[depth_column].values) - - # Create polyline from points - polyline_along_spline = polyline_from_points(points=points_along_spline) - - # Assign log values to polyline - polyline_along_spline['values'] = logs[log].values - - # Create tube from polyline - tube_along_spline = polyline_along_spline.tube(scalars='values', - radius_factor=radius_factor) - - return tube_along_spline - - def calculate_vshale(self, - method: str, - column: str, - column: str, - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None) -> pd.DataFrame: - """Calculate Shale Volume. - - Parameters - __________ - method : str - Method used to calculate the Shale Volume, e.g. ``method='linear'``. Other methods include: - - ==================== ======================================= - ``linear`` Linear Gamma Ray Index Shale Volume - ``larionov_old`` Larionov Shale volume for old rocks - ``larionov_young`` Larionov Shale volume for young rocks - ``clavier`` Clavier Shale volume - ``stieber`` Stieber Shale volume - ==================== ======================================= - - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Returns - _______ - borehole.logs.df : pd.DataFrame - Log DataFrame with appended Shale Volume. - - ============= ========================== - Index Index of each measurement - ---- Remaining columns - Vshale_____ Shale volume - ============= ========================== - - Raises - ______ - TypeError - If the wrong input data types are provided. - ValueError - If a method is chosen that is not available. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_vshale(method='linear', column='GR') - >>> borehole.logs.df - - ======= ====== ==== ============== - Index DEPTH --- VShale_Linear - ======= ====== ==== ============== - 0 0.05 --- 0.037104 - 1 0.10 --- 0.043358 - 2 0.15 --- 0.049788 - 3 0.20 --- 0.055837 - ======= ====== ==== ============== - - See Also - ________ - calculate_vshale_linear : Calculate Shale Volume with the linear method. - calculate_net_to_gross : Calculate Net to Gross value. - - References - __________ - - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. - - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. - - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. - - .. versionadded:: 0.0.1 - """ - # Checking that the method is of type str - if not isinstance(method, str): - raise TypeError('The method must be provided as string') - - # Checking that the provided method is in the list of methods - if method not in ['linear']: - raise ValueError('Provided method is not implemented') - - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Selecting method - if method == 'linear': - self.calculate_vshale_linear(column=column, - minz=minz, - maxz=maxz, - depth_column=depth_column) - - return self.df - - def calculate_vshale_linear(self, - column: str, - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None): - """Calculate Shale Volume with the linear method. - - Parameters - __________ - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_vshale_linear(column='GR') - >>> borehole.logs.df - - ======= ====== ==== ============== - Index DEPTH --- VShale_Linear - ======= ====== ==== ============== - 0 0.05 --- 0.037104 - 1 0.10 --- 0.043358 - 2 0.15 --- 0.049788 - 3 0.20 --- 0.055837 - ======= ====== ==== ============== - - See Also - ________ - calculate_vshale : Calculate Shale Volume. - calculate_net_to_gross : Calculate Net to Gross value. - - .. versionadded:: 0.0.1 - """ - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Checking that the depth column is of type string - if not isinstance(depth_column, (str, type(None))): - raise TypeError('Depth_column must be provided as string') - - # Cropping DataFrame - if None not in (minz, maxz): - df = self.df[(self.df[depth_column] >= minz) & (self.df[depth_column] <= maxz)].copy(deep=True) - else: - df = self.df.copy(deep=True) - - # Obtaining min and max Gamma Ray values - gr_max = df[column].max() - gr_min = df[column].min() - - # Calculating Shale Volume - vshale = (df[column] - gr_min) / (gr_max - gr_min) - - # Appending Shale Volume to Borehole Logs DataFrame - self.df['VShale_Linear'] = vshale - - def calculate_net_to_gross(self, - method: str, - column: str, - cutoff: Union[float, int], - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None) -> float: - """Calculate Net to Gross value. - - Parameters - __________ - method : str - Method used to calculate the Shale Volume, e.g. ``method='linear'``. Other methods include: - - ==================== ======================================= - ``linear`` Linear Gamma Ray Index Shale Volume - ``larionov_old`` Larionov Shale volume for old rocks - ``larionov_young`` Larionov Shale volume for young rocks - ``clavier`` Clavier Shale volume - ``stieber`` Stieber Shale volume - ==================== ======================================= - - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - cutoff : Union[float, int] - Cutoff value for net to gross estimation, e.g. ``cutoff=0.3``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Returns - _______ - net_to_gross : float - Net to gross value. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_net_to_gross(method='linear', column='GR', cutoff=0.3) - 0.5 - - See Also - ________ - calculate_vshale : Calculate Shale Volume. - calculate_vshale_linear : Calculate Shale Volume with the linear method. - - References - __________ - - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. - - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. - - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. - - .. versionadded:: 0.0.1 - """ - # Checking that the method is of type str - if not isinstance(method, str): - raise TypeError('The method must be provided as string') - - # Checking that the provided method is in the list of methods - if method not in ['linear']: - raise ValueError('Provided method is not implemented') - - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the cutoff value is of type float or int - if not isinstance(cutoff, (float, int)): - raise TypeError('The cutoff value must be provided as float or int') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Checking that the depth column is of type string - if not isinstance(depth_column, (str, type(None))): - raise TypeError('Depth_column must be provided as string') - - # Calculating Shale Volume - self.calculate_vshale(method=method, - column=column, - minz=minz, - maxz=maxz, - depth_column=depth_column) - - # Setting column name of Shale Volume - if method == 'linear': - column_vshale = 'VShale_Linear' - - # Assigning net to gross value for each row - def calculating_ng(row, - cutoff_value): - if row <= cutoff_value: - return 1 - elif cutoff_value < row <= 1: - return 0 - else: - return -1 - - # Calculating Net to Gross for each Shale Volume value - self.df['N/G'] = self.df[column_vshale].apply(lambda row: calculating_ng(row, cutoff)) - - # Calculating Net to Gross value - net_to_gross = self.df['N/G'].value_counts()[0] / ( - self.df['N/G'].value_counts()[0] + self.df['N/G'].value_counts()[1]) - - return net_to_gross - - def add_curve_to_curve(self, - original_mnemonic: str, - mnemonic: str, - descr: str, - unit: str): - """Add curve to curves DataFrame so it can also be used for plotting. - - Parameters - __________ - original_mnemonic : str - Original mnemonic of the track. - mnemonic : str - Mnemonic of the track. - descr : str - Description of the track. - unit : str - Unit of the data of the track. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> borehole.logs.add_curve_to_curves(original_mnemonic='Velocity', mnemonic='Velocity', descr='Velocity', unit='m/s') - >>> borehole.logs.curves - - .. versionadded:: 0.0.1 - """ - - # Checking that the original mnemonic is provided as string - if not isinstance(original_mnemonic, str): - raise TypeError('original_mnemonic must be provided as string') - - # Checking that the mnemonic is provided as string - if not isinstance(mnemonic, str): - raise TypeError('mnemonic must be provided as string') - - # Checking that the description is provided as string - if not isinstance(descr, str): - raise TypeError('descr must be provided as string') - - # Checking that the unit is provided as string - if not isinstance(unit, str): - raise TypeError('unit must be provided as string') - - # Checking that track is not available in the curves DataFrame - if original_mnemonic in self.curves: - raise ValueError('Track is already present in Curve DataFrame') - - # Concatenate new curve to existing curves - self.curves = pd.concat([self.curves, - pd.DataFrame.from_dict( - {'original_mnemonic': [original_mnemonic], - 'mnemonic': [mnemonic], - 'descr': [descr], - 'unit': [unit]}), - ], - ignore_index=True) - - def despike_curve(self, - track: str, - window_length: int): - - """Despike curve using a rolling window. - - - """ - - self.df[track + '_rolling'] = self.df.rolling(window=window_length)[track].mean() - - curve_df = self.curves[self.curves['original_mnemonic'] == track].reset_index(drop=True).iloc[0] - - self.add_curve_to_curve(original_mnemonic=track + '_rolling', - mnemonic=curve_df['mnemonic'] + '_rolling', - descr=curve_df['descr'] + '_rolling', - unit=curve_df['unit']) - - def calculate_time_depth_relationship(self, - track: str, - depth_column: str = 'DEPTH', - replacement_velocity: Union[int, float] = 2000): - log_start = self.df[depth_column].iloc[0] - altitude = self._borehole.altitude_above_sea_level - gap_int = log_start - altitude - log_start_time = 2 * gap_int / replacement_velocity - - increment = \ - self.well_header[self.well_header['mnemonic'] == 'STEP'].reset_index(drop=True)['value'].iloc[0] - - dt_interval = np.nan_to_num(self.df[track].values) * increment / 1e6 - t_cum = np.cumsum(dt_interval) * 2 - self.df['TWT'] = t_cum + log_start_time - - self.add_curve_to_curve(original_mnemonic='TWT', - mnemonic='TWT', - descr='TWT', - unit='s') - - def calculate_acoustic_impedance(self, - velocity_track: Union[str, np.ndarray], - density_track: Union[str, np.ndarray], - add_to_well_logs: bool = True): - - if isinstance(velocity_track, str) and isinstance(density_track, str): - velocity = self.df[velocity_track].values - density = self.df[density_track].values - - values = np.c_[velocity, density] - - if isinstance(velocity_track, np.ndarray) and isinstance(density_track, np.ndarray): - values = np.c_[velocity_track, density_track] - - acoustic_impedance = np.apply_along_axis(np.product, -1, - values) - - if add_to_well_logs: - self.df['Acoustic Impedance'] = acoustic_impedance - - self.add_curve_to_curve(original_mnemonic='Acoustic Impedance', - mnemonic='Acoustic Impedance', - descr='Acoustic Impedance', - unit=' ') - else: - return acoustic_impedance - - def calculate_reflection_coefficient(self, - velocity_track: Union[str, np.ndarray], - density_track: Union[str, np.ndarray], - add_to_well_logs: bool = True): - - if add_to_well_logs: - self.calculate_acoustic_impedance(velocity_track=velocity_track, - density_track=density_track, - add_to_well_logs=add_to_well_logs) - acoustic_impedance = self.df['Acoustic Impedance'].values - - else: - acoustic_impedance = self.calculate_acoustic_impedance(velocity_track=velocity_track, - density_track=density_track, - add_to_well_logs=add_to_well_logs) - - reflection_coefficient = (acoustic_impedance[1:] - acoustic_impedance[:-1]) / ( - acoustic_impedance[1:] + acoustic_impedance[:-1]) - - reflection_coefficient = np.append(reflection_coefficient, 0) - - if add_to_well_logs: - - self.df['Reflection Coefficient'] = reflection_coefficient - - self.add_curve_to_curve(original_mnemonic='Reflection Coefficient', - mnemonic='Reflection Coefficient', - descr='Reflection Coefficient', - unit=' ') - - else: - return reflection_coefficient - - def resample_log_from_depth_to_time_domain(self, - dt: float, - track: str): - - if 'TWT' not in self.df.columns: - raise ValueError( - 'Please calculate time-depth relationship first before resampling log from depth to the time domain') - - t_max = self.df['TWT'].iloc[-1] - t = np.arange(0, t_max, dt) - - log_time_domain = np.interp(x=t, - xp=self.df['TWT'], - fp=self.df[track]) - - return log_time_domain - - def calculate_ricker_wavelet(self, - f, - length, - dt): - t0 = np.arange(-length / 2, (length - dt) / 2, dt) - y = (1.0 - 2.0 * (np.pi ** 2) * (f ** 2) * (t0 ** 2)) * np.exp(-(np.pi ** 2) * (f ** 2) * (t0 ** 2)) - return t0, y - - def calculate_synthetic_seismic(self, - t0, - w, - rc_tdom): - synth = np.apply_along_axis(lambda t0: np.convolve(t0, w, mode="same"), axis=0, arr=rc_tdom) - - return synth - - -class DLISLogs: - """Class to initiate a Well Log Object. - - Parameters - __________ - path : str - Path to the well logs, e.g. ``path='logs.dlis'``. - nodata : Union[int, float], default: ``-9999`` - No data value, e.g. ``nodata=-9999``. - - Attributes - __________ - df : pd.DataFrame - Pandas DataFrame containing the curves. Data columns are as follows: - - ======= ===================================== - Index Index of each measurment - DEPTH Measured depth of each measurment - Curves Columns containing the measurements - ======= ===================================== - - **Example:** - - ======= ======= =========== ========= ========= ========== - Index DEPTH SGR.SG TH.SG U.SG K.SG - ======= ======= =========== ========= ========= ========== - 0 0.02 NaN NaN NaN NaN - 1 0.07 9.997668 1.991596 0.409476 0.247140 - 2 0.12 10.925511 2.279886 0.427854 0.262401 - 3 0.17 11.815056 2.566926 0.447979 0.273815 - 4 0.22 12.674678 2.846440 0.467957 0.284187 - ======= ======= =========== ========= ========= ========== - - Returns - _______ - DLISLogs - Well Log Object. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.df - - ======= ======= =========== ========= ========= ========== - Index DEPTH SGR.SG TH.SG U.SG K.SG - ======= ======= =========== ========= ========= ========== - 0 0.02 NaN NaN NaN NaN - 1 0.07 9.997668 1.991596 0.409476 0.247140 - 2 0.12 10.925511 2.279886 0.427854 0.262401 - 3 0.17 11.815056 2.566926 0.447979 0.273815 - 4 0.22 12.674678 2.846440 0.467957 0.284187 - ======= ======= =========== ========= ========= ========== - - See Also - ________ - pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. - pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. - pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. - pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. - - .. versionadded:: 0.0.1 - """ - - # Initiate class - def __init__(self, - borehole, - path: str, - nodata: Union[int, float] = -9999): - """Class to initiate a Well Log Object. - - Parameters - __________ - path : str - Path to the well logs, e.g. ``path='logs.dlis'``. - nodata : Union[int, float], default: ``-9999`` - No data value, e.g. ``nodata=-9999``. - - Attributes - __________ - df : pd.DataFrame - Pandas DataFrame containing the curves. Data columns are as follows: - - ======= ===================================== - Index Index of each measurment - DEPTH Measured depth of each measurment - Curves Columns containing the measurements - ======= ===================================== - - **Example:** - - ======= ======= =========== ========= ========= ========== - Index DEPTH SGR.SG TH.SG U.SG K.SG - ======= ======= =========== ========= ========= ========== - 0 0.02 NaN NaN NaN NaN - 1 0.07 9.997668 1.991596 0.409476 0.247140 - 2 0.12 10.925511 2.279886 0.427854 0.262401 - 3 0.17 11.815056 2.566926 0.447979 0.273815 - 4 0.22 12.674678 2.846440 0.467957 0.284187 - ======= ======= =========== ========= ========= ========== - - Returns - _______ - DLISLogs - Well Log Object. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.dlis') - >>> borehole.logs.df - - ======= ======= =========== ========= ========= ========== - Index DEPTH SGR.SG TH.SG U.SG K.SG - ======= ======= =========== ========= ========= ========== - 0 0.02 NaN NaN NaN NaN - 1 0.07 9.997668 1.991596 0.409476 0.247140 - 2 0.12 10.925511 2.279886 0.427854 0.262401 - 3 0.17 11.815056 2.566926 0.447979 0.273815 - 4 0.22 12.674678 2.846440 0.467957 0.284187 - ======= ======= =========== ========= ========= ========== - - See Also - ________ - pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. - pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. - pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. - pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. - - .. versionadded:: 0.0.1 - """ - # Importing dlisio - try: - from dlisio import dlis - except ModuleNotFoundError: - ModuleNotFoundError('dlisio package not installed') - - # Checking that the path is provided as string - if not isinstance(path, str): - raise TypeError('The path must be provided as string') - - # Checking that the nodata is provided as float or int - if not isinstance(nodata, (float, int)): - raise TypeError('The nodata value must be provided as float or int') - - # Opening DLIS file - dlis, *tail = dlis.load(path) - - # Getting column names - columns = [channel.name for channel in dlis.channels] - - # Getting Curves - curves = [channel.curves() for channel in dlis.channels] - - # Getting Units - units = [channel.units for channel in dlis.channels] - - # Creating DataFrame from curves - df = pd.DataFrame(curves).T - - # Assigning column names - df.columns = columns - - # Replace NaN Values - df = df.replace(nodata, np.NaN) - - # Extracting DataFrame from DLIS file - self.df = df - - # Changing the name of the depth column - self.df['DEPTH'] = self.df['DEPT'] - self.df = self.df.drop('DEPT', axis=1) - - # Creating Curves DataFrame - self.curves = pd.DataFrame(list(zip(columns, columns, columns, units)), - columns=['original_mnemonic', - 'mnemonic', - 'descr', - 'unit']) - - # Getting index of depth column - depth_column_index = columns.index('DEPT') - - # Defining mnemonics - mnemonic = ['STRT', 'STOP', 'STEP', 'NULL'] - - # Defining units - units = [dlis.channels[depth_column_index].units, - dlis.channels[depth_column_index].units, - dlis.channels[depth_column_index].units, - ''] - - # Defining Values - value = [dlis.channels[depth_column_index].curves()[0], - dlis.channels[depth_column_index].curves()[-1], - dlis.channels[depth_column_index].frame.spacing, - -999] - - # Defining Description - descr = ['', '', '', ''] - - self.well_header = pd.DataFrame(list(zip(mnemonic, units, value, descr)), - columns=['mnemonic', - 'unit', - 'value', - 'descr']) - - def plot_well_logs(self, - tracks: Union[str, list], - depth_column: str = 'DEPTH', - depth_min: Union[int, float] = None, - depth_max: Union[int, float] = None, - colors: Union[str, list] = None, - add_well_tops: bool = False, - add_well_design: bool = False, - fill_between: int = None, - add_net_to_gross: int = None) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: - """Plot well logs. - - Parameters - __________ - - tracks : Union[str, list] - Name/s of the logs to be plotted, e.g. ``tracks='SGR'`` or ``tracks=['SGR', 'K']``. - depth_column : str, default: ``'DEPTH'`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - depth_min : Union[int, float] - Minimum depth to be plotted, e.g. ``depth_min=0``. - depth_max : Union[int, float] - Maximum depth to be plotted, e.g. ``depth_max=2000``. - colors : Union[str, list], default: ``None`` - Colors of the logs, e.g. ``colors='black'`` or ``colors=['black', 'blue']``. - add_well_tops : bool, default: ``False`` - Boolean to add well tops to the plot, e.g. ``add_well_tops=True``. - add_well_design : bool, default: ``False`` - Boolean to add well design to the plot, e.g. ``add_well_design=True``. - fill_between : int, default: ``None`` - Number of the axis to fill, e.g. ``fill_between=0``. - add_net_to_gross : int, default: ``None`` - Number of axis to fill with the net to gross values, e.g. ``add_net_to_gross=0``. - - Returns - _______ - fig : matplotlib.figure.Figure - Matplotlib Figure. - ax : matplotlib.axes.Axes - Matplotlib Axes. - - Raises - ______ - TypeError - If the wrong input data types are provided. - ValueError - If no well tops are provided but ``add_well_tops`` is set to ``True``. - ValueError - If the wrong column names are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.plot_well_logs(tracks=['SGR', 'CAL'], depth_column='DEPTH', colors=['black', 'red']) - - See Also - ________ - plot_well_log_along_path : Plot well log along path. - calculate_vshale : Calculate Shale Volume. - calculate_vshale_linear : Calculate Shale Volume linear method. - calculate_net_to_gross : Calculate net to gross. - - .. versionadded:: 0.0.1 - """ - # Checking that the tracks are provided as list or string - if not isinstance(tracks, (list, str)): - raise TypeError('The track/s must bei either provided as str or a list of strings') - - # Checking that the tracks are in the DataFrame - if isinstance(tracks, str): - if not {tracks}.issubset(self.df.columns): - raise ValueError('The track is not part of the curves') - elif isinstance(tracks, list): - if not all({track}.issubset(self.df.columns) for track in tracks): - raise ValueError('Not all tracks are part of the curves') - - # Checking that the depth column is of type string - if not isinstance(depth_column, str): - raise TypeError('Depth_column must be provided as string') - - # Checking that depth_min is of type float or int - if not isinstance(depth_min, (int, float, type(None))): - raise ValueError('The minimum depth must be provided as int or float') - - # Checking that depth_max is of type float or int - if not isinstance(depth_max, (int, float, type(None))): - raise ValueError('The maximum depth must be provided as int or float') - - # Checking that the depth column is in the DataFrame - if not {depth_column}.issubset(self.df): - raise ValueError('The depth_column is not part of the curves') - - # Checking that the colors are provided as list or string - if not isinstance(colors, (list, str, type(None))): - raise TypeError('The track/s must bei either provided as str or a list of strings') - - # Checking that the add_well_tops variable is of type bool - if not isinstance(add_well_tops, bool): - raise TypeError('The add_well_tops variable must be provided as bool') - - # Checking that the add_well_design variable is of type bool - if not isinstance(add_well_design, bool): - raise TypeError('The add_well_design variable must be provided as bool') - - # Checking that the fill_between variable is of type int - if not isinstance(fill_between, (int, type(None))): - raise TypeError('The fill_between variable must be provided as int') - - # Checking that the add_net to_gross variable is of type bool - if not isinstance(add_net_to_gross, (int, type(None))): - raise TypeError('The add_net_to_gross variable must be provided as int') - - # Selecting tracks - df = self.df[tracks + [depth_column]].reset_index() - - # Creating plot if tracks is of type string - if isinstance(tracks, str): - # Creating plot - fig, ax = plt.subplots(1, 1, figsize=(1 * 2, 8)) - - ax.plot(df[tracks], df[depth_column], color=colors) - ax.grid() - ax.invert_yaxis() - - if not depth_min: - depth_min = min(df[depth_column]) - if not depth_max: - depth_max = max(df[depth_column]) - - buffer = (depth_max - depth_min) / 20 - - ax.set_ylim(depth_max + buffer, depth_min - buffer) - ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) - ax.xaxis.set_label_position('top') - ax.set_xlabel(tracks + ' [%s]' % - self.curves[self.curves['original_mnemonic'] == tracks].reset_index(drop=True)['unit'].iloc[ - 0], color='black') - ax.set_ylabel(depth_column + ' [m]') - - # Fill between curve - if fill_between: - left_col_value = np.min(df[tracks].dropna().values) - right_col_value = np.max(df[tracks].dropna().values) - span = abs(left_col_value - right_col_value) - cmap = plt.get_cmap('hot_r') - color_index = np.arange(left_col_value, right_col_value, span / 100) - # loop through each value in the color_index - for index in sorted(color_index): - index_value = (index - left_col_value) / span - color = cmap(index_value) # obtain color for color index value - ax.fill_betweenx(df[depth_column], df[tracks], left_col_value, where=df[tracks] >= index, - color=color) - - return fig, ax - - # Creating plot if tracks is of type list - elif isinstance(tracks, list): - - # Helping variable for adding well tops - if add_well_tops: - j = 1 - else: - j = 0 - - if add_well_design: - k = 1 - else: - k = 0 - - # Setting colors - if not colors: - colors = [None] * len(tracks) - - # Creating plot - fig, ax = plt.subplots(1, - len(tracks) + j + k, - figsize=((len(tracks) + j + k) * 1.8, 8), - sharey=True) - - # Adding well tops - if add_well_tops: - if not self.has_well_tops: - raise ValueError('Please add well tops to the borehole object to plot them') - else: - for index, row in self.well_tops.df.iterrows(): - ax[0 + k].axhline(row[self.well_tops.df.columns[1]], 0, 1, color='black') - ax[0 + k].text(0.05, row[self.well_tops.df.columns[1]] - 1, s=row[self.well_tops.df.columns[0]], - fontsize=6) - ax[0 + k].grid() - ax[0 + k].axes.get_xaxis().set_ticks([]) - - # Adding well design - if add_well_design: - if not self.has_well_design: - raise ValueError('Please add a well design to the borehole object to plot it') - else: - for key, elem in self.well_design.pipes.items(): - if elem.pipe_type == 'open hole section': - # Plotting open hole section as wavy line - y = np.linspace(elem.top, elem.bottom, 1000) - x1 = 0.5 * np.sin(y / 2) - elem.inner_diameter + 0.5 - x2 = 0.5 * np.cos(y / 2 + np.pi / 4) + elem.inner_diameter - ax[k].plot(x1, y, color='black') - ax[k].plot(x2, y, color='black') - # Plotting open hole section as dashed line - # ax.plot([elem.inner_diameter,elem.inner_diameter], - # [elem.top, elem.bottom], - # linestyle='--', - # color='black') - # ax.plot([-elem.inner_diameter, -elem.inner_diameter], - # [elem.top, elem.bottom], - # linestyle='--', - # color='black') - else: - ax[k].add_patch(Rectangle(elem.xy, elem.width, elem.height, color="black")) - ax[k].add_patch( - Rectangle((-1 * elem.xy[0], elem.xy[1]), -1 * elem.width, elem.height, color="black")) - - # Deep copy of pipes dict - x = copy.deepcopy(self.pipes) - - # Popping open hole section - try: - type_list = [elem.pipe_type for key, elem in x.items()] - index_open_hole = type_list.index('open hole section') - key_open_hole = list(x.keys())[index_open_hole] - x.pop(key_open_hole) - except ValueError: - pass - - # Getting diameters and calculating thickness of cement between each pipe - outer_diameters = sorted([elem.outer_diameter for key, elem in x.items()], reverse=False) - inner_diameters = sorted([elem.inner_diameter for key, elem in x.items()], reverse=False) - thicknesses = [y - x for x, y in zip(outer_diameters[:-1], inner_diameters[1:])] - - # Sorting pipes - pipes_sorted = {k: v for k, v in sorted(x.items(), key=lambda item: item[1].outer_diameter)} - - # Selecting pies - pipes_selected = {k: pipes_sorted[k] for k in list(pipes_sorted)[:len(thicknesses)]} - - # Plotting top of pipe - i = 0 - for key, elem in pipes_selected.items(): - i = i - ax[k].add_patch( - Rectangle((elem.inner_diameter, elem.top), thicknesses[i] + elem.thickness, 1, - color="black")) - ax[k].add_patch(Rectangle((-1 * elem.inner_diameter, elem.top), - -1 * thicknesses[0] - elem.thickness, 1, - color="black")) - i = i + 1 - - # Plotting Casing Shoes - for key, elem in self.well_design.pipes.items(): - if elem.shoe_width is not None: - p0 = [elem.outer_diameter, elem.bottom] - p1 = [elem.outer_diameter, elem.bottom - elem.shoe_height] - p2 = [elem.outer_diameter + elem.shoe_width, elem.bottom] - shoe = plt.Polygon([p0, p1, p2], color="black") - ax.add_patch(shoe) - p0[0] *= -1 - p1[0] *= -1 - p2 = [-elem.outer_diameter - elem.shoe_width, elem.bottom] - shoe = plt.Polygon([p0, p1, p2], color="black") - ax[k].add_patch(shoe) - - # Adding Casing Cements - for key, elem in self.well_design.cements.items(): - ax[k].fill_between(elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) - ax[k].fill_between(-1 * elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) - - # Calculating axes limits - top = np.min([elem.top for key, elem in self.well_design.pipes.items()]) - bottom = np.max([elem.bottom for key, elem in self.well_design.pipes.items()]) - max_diam = np.max([elem.outer_diameter for key, elem in self.well_design.pipes.items()]) - - # Setting axes limits - if not depth_min: - depth_min = min(df[depth_column]) - if not depth_max: - depth_max = max(df[depth_column]) - - buffer = (depth_max - depth_min) / 20 - - ax[k].set_ylim(depth_max + buffer, depth_min - buffer) - ax.set_xlim(-max_diam * 1, max_diam * 1) - # ax.invert_yaxis() - - # Setting axes labels - ax.set_xlabel('Diameter [in]') - - # Plotting tracks - for i in range(len(tracks)): - ax[i + j + k].plot(df[tracks[i]], df[depth_column], color=colors[i]) - ax[i + j + k].grid() - ax[i + j + k].invert_yaxis() - buffer = (max(df[depth_column]) - min(df[depth_column])) / 20 - ax[i + j + k].set_ylim(max(df[depth_column]) + buffer, min(df[depth_column]) - buffer) - ax[i + j + k].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) - ax[i + j + k].xaxis.set_label_position('top') - ax[i + j + k].set_xlabel(tracks[i] + ' [%s]' % - self.curves[self.curves['original_mnemonic'] == tracks[i]].reset_index( - drop=True)[ - 'unit'].iloc[0], - color='black' if isinstance(colors[i], type(None)) else colors[i]) - ax[0].set_ylabel(depth_column + ' [m]') - - # Fill between curves - if fill_between is not None: - left_col_value = np.min(df[tracks[fill_between]].dropna().values) - right_col_value = np.max(df[tracks[fill_between]].dropna().values) - span = abs(left_col_value - right_col_value) - cmap = plt.get_cmap('hot_r') - color_index = np.arange(left_col_value, right_col_value, span / 100) - - # Dropping duplicate columns if the same track is selected twice - try: - df1 = df[tracks[fill_between]].copy(deep=True) - df1 = df1.loc[:, ~df1.columns.duplicated()] - except AttributeError: - df1 = df[tracks[fill_between]].copy(deep=True) - - # loop through each value in the color_index - for index in sorted(color_index): - index_value = (index - left_col_value) / span - color = cmap(index_value) # obtain color for color index value - ax[fill_between + j].fill_betweenx(df[depth_column], - # - df1.values.flatten(), - left_col_value, - where=df1.values.flatten() >= index, - color=color) - - # Adding net to gross - if add_net_to_gross is not None: - if 'N/G' not in self.df.columns: - raise ValueError('Net to gross has not been calculated yet') - - ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], - self.df[tracks[add_net_to_gross]], - 0, - where=self.df['N/G'] == 1, - color='yellow') - ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], - self.df[tracks[add_net_to_gross]], - 0, - where=self.df['N/G'] == 0, - color='brown') - - plt.tight_layout() - - return fig, ax - - def plot_well_log_along_path(self, - log: str, - depth_column: str = 'DEPTH', - relative: bool = True, - spacing: Union[float, int] = 0.5, - radius_factor: Union[float, int] = 75): - """Plot well log along path. - - Parameters - __________ - log : str - Name of the log, e.g. ``log='GR'``. - depth_column : str, default: ``'DEPTH'`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - relative : bool, default: ``True`` - Boolean value to plot the tube with relative coordinates, e.g. ``relative=False``. - spacing : Union[float, int], default: ``0.5`` - Spacing for the resampling of the well path, e.g. ``spacing=0.5``. - radius_factor : Union[float, int], default: ``75`` - Factor to scale the radius of the tube, e.g. ``radius_factor=75``. - - Return - ______ - tube : pv.core.pointset.PolyData - PyVista Tube of the borehole. - - Raises - ______ - ModuleNotFoundError - Raises error if PyVista is not installed. - TypeError - If the wrong input data types are provided. - ValueError - Raises error if the wrong column names are provided. - ValueError - Raises error if no ``Deviation`` has been defined. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs(path='Well_logs.las') - >>> borehole.logs.plot_well_log_along_path(log='SGR', depth_column='DEPTH', spacing=0.5, radius_factor=10) - - =========== ========================= - PolyData Information - =========== ========================= - N Cells 22 - N Points 40040 - N Strips 22 - X Bounds 3.413e+06, 3.413e+06 - Y Bounds 5.836e+06, 5.836e+06 - Z Bounds -9.925e+01, -5.000e-02 - N Arrays 2 - =========== ========================= - - ============ ======= ======== ======= =========== ========== - Name Field Type N Comp Min Max - ============ ======= ======== ======= =========== ========== - values Points float64 1 4.342e+00 1.474e+02 - TubeNormals Points float32 3 -1.000e+00 1.000e+00 - ============ ======= ======== ======= =========== ========== - - See Also - ________ - plot_well_logs : Plot well logs. - - .. versionadded:: 0.0.1 - """ - # Importing pyvista - try: - import pyvista as pv - except ModuleNotFoundError: - ModuleNotFoundError('PyVista package not installed') - - # Checking that log is provided as string - if not isinstance(log, str): - raise TypeError('The name of the log must be provided as str') - - # Checking that the depth column is of type string - if not isinstance(depth_column, str): - raise TypeError('Depth_column must be provided as string') - - # Checking that the relative value is provided as bool - if not isinstance(relative, bool): - raise TypeError('The relative value must be provided as bool') - - # Checking that the spacing is of type float or int - if not isinstance(spacing, (float, int)): - raise TypeError('The spacing for the resampling must be provided as float or int') - - # Checking that the radius_factor is of type float or int - if not isinstance(radius_factor, (float, int)): - raise TypeError('The radius_factor must be provided as float or int') - - # Checking if the borehole deviation is present - if not self._borehole.deviation: - raise ValueError('Borehole deviation is needed to create a tube') - - # Extracting the coordinates - coordinates = self._borehole.deviation.desurveyed_df.copy(deep=True) - - if relative: - if not {'Northing_rel', 'Easting_rel', 'True Vertical Depth'}.issubset(coordinates.columns): - raise ValueError( - 'The coordinates DataFrame must contain Northing_rel, Easting_rel and True Vertical Depth ' - 'below sea level column') - - coordinates['True Vertical Depth'] = coordinates['True Vertical Depth'] * (-1) - xyz = coordinates[['Easting_rel', 'Northing_rel', 'True Vertical Depth']].to_numpy() - - else: - if not {'Northing', 'Easting', 'True Vertical Depth Below Sea Level'}.issubset(coordinates.columns): - raise ValueError( - 'The coordinates DataFrame must contain Northing, Easting and True Vertical Depth Below Sea Level ' - 'below sea level column') - - coordinates['True Vertical Depth Below Sea Level'] = coordinates['True Vertical Depth Below Sea Level'] * ( - -1) - xyz = coordinates[['Easting', 'Northing', 'True Vertical Depth Below Sea Level']].to_numpy() - - # Obtaining log values - logs = self.df.reset_index()[[depth_column, log]] - - # Resample points - points = resample_between_well_deviation_points(coordinates=xyz, - spacing=spacing) - - # Creating spline - polyline_well_path_resampled = pv.Spline(points) - - # Getting points along the spline - points_along_spline = get_points_along_spline(spline=polyline_well_path_resampled, - dist=logs[depth_column].values) - - # Create polyline from points - polyline_along_spline = polyline_from_points(points=points_along_spline) - - # Assign log values to polyline - polyline_along_spline['values'] = logs[log].values - - # Create tube from polyline - tube_along_spline = polyline_along_spline.tube(scalars='values', - radius_factor=radius_factor) - - return tube_along_spline - - def calculate_vshale(self, - method: str, - column: str, - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None) -> pd.DataFrame: - """Calculate Shale Volume. - - Parameters - __________ - method : str - Method used to calculate the Shale Volume, e.g. ``method='linear'``. - The following methods are available - - ==================== ======================================= - ``linear`` Linear Gamma Ray Index Shale Volume - ``larionov_old`` Larionov Shale volume for old rocks - ``larionov_young`` Larionov Shale volume for young rocks - ``clavier`` Clavier Shale volume - ``stieber`` Stieber Shale volume - ==================== ======================================= - - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Returns - _______ - borehole.logs.df : pd.DataFrame - Log DataFrame with appended Shale Volume. - - ============= ========================== - Index Index of each measurement - ---- Remaining columns - Vshale_____ Shale volume - ============= ========================== - - Raises - ______ - TypeError - If the wrong input data types are provided. - ValueError - If a method is chosen that is not available. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_vshale(method='linear', column='GR') - >>> borehole.logs.df - - ======= ====== ==== ============== - Index DEPTH --- VShale_Linear - ======= ====== ==== ============== - 0 0.05 --- 0.037104 - 1 0.10 --- 0.043358 - 2 0.15 --- 0.049788 - 3 0.20 --- 0.055837 - ======= ====== ==== ============== - - See Also - ________ - calculate_vshale_linear : Calculate Shale Volume with the linear method. - calculate_net_to_gross : Calculate Net to Gross value. - - References - __________ - - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. - - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. - - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. - - .. versionadded:: 0.0.1 - """ - # Checking that the method is of type str - if not isinstance(method, str): - raise TypeError('The method must be provided as string') - - # Checking that the provided method is in the list of methods - if method not in ['linear']: - raise ValueError('Provided method is not implemented') - - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Selecting method - if method == 'linear': - self.calculate_vshale_linear(column=column, - minz=minz, - maxz=maxz, - depth_column=depth_column) - - return self.df - - def calculate_vshale_linear(self, - column: str, - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None): - """Calculate Shale Volume with the linear method. - - Parameters - __________ - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_vshale_linear(column='GR') - >>> borehole.logs.df - - ======= ====== ==== ============== - Index DEPTH --- VShale_Linear - ======= ====== ==== ============== - 0 0.05 --- 0.037104 - 1 0.10 --- 0.043358 - 2 0.15 --- 0.049788 - 3 0.20 --- 0.055837 - ======= ====== ==== ============== - - See Also - ________ - calculate_vshale : Calculate Shale Volume. - calculate_net_to_gross : Calculate Net to Gross value. - - .. versionadded:: 0.0.1 - """ - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Checking that the depth column is of type string - if not isinstance(depth_column, (str, type(None))): - raise TypeError('Depth_column must be provided as string') - - # Cropping DataFrame - if None not in (minz, maxz): - df = self.df[(self.df[depth_column] >= minz) & (self.df[depth_column] <= maxz)].copy(deep=True) - else: - df = self.df.copy(deep=True) - - # Obtaining min and max Gamma Ray values - gr_max = df[column].max() - gr_min = df[column].min() - - # Calculating Shale Volume - vshale = (df[column] - gr_min) / (gr_max - gr_min) - - # Appending Shale Volume to Borehole Logs DataFrame - self.df['VShale_Linear'] = vshale - - def calculate_net_to_gross(self, - method: str, - column: str, - cutoff: Union[float, int], - minz: Union[float, int] = None, - maxz: Union[float, int] = None, - depth_column: str = None) -> float: - """Calculate Net to Gross value. - - Parameters - __________ - method : str - Method used to calculate the Shale Volume, e.g. ``method='linear'``. - - ==================== ======================================= - ``linear`` Linear Gamma Ray Index Shale Volume - ``larionov_old`` Larionov Shale volume for old rocks - ``larionov_young`` Larionov Shale volume for young rocks - ``clavier`` Clavier Shale volume - ``stieber`` Stieber Shale volume - ==================== ======================================= - - column : str - Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. - cutoff : Union[float, int] - Cutoff value for net to gross estimation, e.g. ``cutoff=0.3``. - minz : Union[float, int], default: ``None`` - Minimum Z value, e.g. ``minz=50``. - maxz : Union[float, int], default: ``None`` - Maximum Z value, e.g. ``maxz=100``. - depth_column : str, default: ``None`` - Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. - - Returns - _______ - net_to_gross : float - Net to gross value. - - Raises - ______ - TypeError - If the wrong input data types are provided. - - Examples - ________ - >>> import pyborehole - >>> from pyborehole.borehole import Borehole - >>> borehole = Borehole(name='Weisweiler R1') - >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) - >>> borehole.add_well_logs('Well_Logs.las') - >>> borehole.logs.calculate_net_to_gross(method='linear', column='GR', cutoff=0.3) - 0.5 - - See Also - ________ - calculate_vshale : Calculate Shale Volume. - calculate_vshale_linear : Calculate Shale Volume with the linear method. - - References - __________ - - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. - - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. - - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. - - .. versionadded:: 0.0.1 - """ - # Checking that the method is of type str - if not isinstance(method, str): - raise TypeError('The method must be provided as string') - - # Checking that the provided method is in the list of methods - if method not in ['linear']: - raise ValueError('Provided method is not implemented') - - # Checking that the column is of type str - if not isinstance(column, str): - raise TypeError('The column name must be provided as string') - - # Checking that the cutoff value is of type float or int - if not isinstance(cutoff, (float, int)): - raise TypeError('The cutoff value must be provided as float or int') - - # Checking that the minz value is of type float or int - if not isinstance(minz, (float, int, type(None))): - raise TypeError('minz must be provided as float or int') - - # Checking that the maxz value is of type float or int - if not isinstance(maxz, (float, int, type(None))): - raise TypeError('maxz must be provided as float or int') - - # Checking that the depth column is of type string - if not isinstance(depth_column, (str, type(None))): - raise TypeError('Depth_column must be provided as string') - - # Calculating Shale Volume - self.calculate_vshale(method=method, - column=column, - minz=minz, - maxz=maxz, - depth_column=depth_column) - - # Setting column name of Shale Volume - if method == 'linear': - column_vshale = 'VShale_Linear' - - # Assigning net to gross value for each row - def calculating_ng(row, - cutoff_value): - if row <= cutoff_value: - return 1 - elif cutoff_value < row <= 1: - return 0 - else: - return -1 - - # Calculating Net to Gross for each Shale Volume value - self.df['N/G'] = self.df[column_vshale].apply(lambda row: calculating_ng(row, cutoff)) - - # Calculating Net to Gross value - net_to_gross = self.df['N/G'].value_counts()[0] / ( - self.df['N/G'].value_counts()[0] + self.df['N/G'].value_counts()[1]) - - return net_to_gross - - def resample_between_well_deviation_points(coordinates: np.ndarray, spacing: Union[float, int]) -> np.ndarray: """Resample between points that define the path of a well. diff --git a/pyborehole/logs_dlis.py b/pyborehole/logs_dlis.py new file mode 100644 index 0000000..6398896 --- /dev/null +++ b/pyborehole/logs_dlis.py @@ -0,0 +1,1058 @@ +from typing import Union, Tuple +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Rectangle +import matplotlib +import copy + +from pyborehole.logs import resample_between_well_deviation_points, get_points_along_spline, polyline_from_points + +class DLISLogs: + """Class to initiate a Well Log Object. + + Parameters + __________ + path : str + Path to the well logs, e.g. ``path='logs.dlis'``. + nodata : Union[int, float], default: ``-9999`` + No data value, e.g. ``nodata=-9999``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + Returns + _______ + DLISLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.dlis') + >>> borehole.logs.df + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + See Also + ________ + pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. + pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. + pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. + pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. + + .. versionadded:: 0.0.1 + """ + + # Initiate class + def __init__(self, + borehole, + path: str, + nodata: Union[int, float] = -9999): + """Class to initiate a Well Log Object. + + Parameters + __________ + path : str + Path to the well logs, e.g. ``path='logs.dlis'``. + nodata : Union[int, float], default: ``-9999`` + No data value, e.g. ``nodata=-9999``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + Returns + _______ + DLISLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.dlis') + >>> borehole.logs.df + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + See Also + ________ + pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. + pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. + pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. + pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. + + .. versionadded:: 0.0.1 + """ + # Importing dlisio + try: + from dlisio import dlis + except ModuleNotFoundError: + ModuleNotFoundError('dlisio package not installed') + + # Checking that the path is provided as string + if not isinstance(path, str): + raise TypeError('The path must be provided as string') + + # Checking that the nodata is provided as float or int + if not isinstance(nodata, (float, int)): + raise TypeError('The nodata value must be provided as float or int') + + # Opening DLIS file + dlis, *tail = dlis.load(path) + + # Getting column names + columns = [channel.name for channel in dlis.channels] + + # Getting Curves + curves = [channel.curves() for channel in dlis.channels] + + # Getting Units + units = [channel.units for channel in dlis.channels] + + # Creating DataFrame from curves + df = pd.DataFrame(curves).T + + # Assigning column names + df.columns = columns + + # Replace NaN Values + df = df.replace(nodata, np.NaN) + + # Extracting DataFrame from DLIS file + self.df = df + + # Changing the name of the depth column + self.df['DEPTH'] = self.df['DEPT'] + self.df = self.df.drop('DEPT', axis=1) + + # Creating Curves DataFrame + self.curves = pd.DataFrame(list(zip(columns, columns, columns, units)), + columns=['original_mnemonic', + 'mnemonic', + 'descr', + 'unit']) + + # Getting index of depth column + depth_column_index = columns.index('DEPT') + + # Defining mnemonics + mnemonic = ['STRT', 'STOP', 'STEP', 'NULL'] + + # Defining units + units = [dlis.channels[depth_column_index].units.strip(), + dlis.channels[depth_column_index].units.strip(), + dlis.channels[depth_column_index].units, + ''] + + # Defining Values + value = [dlis.channels[depth_column_index].curves()[0], + dlis.channels[depth_column_index].curves()[-1], + dlis.channels[depth_column_index].frame.spacing, + nodata] + + # Defining Description + descr = ['', '', '', ''] + + # Creating Well Header DataFrame + self.well_header = pd.DataFrame(list(zip(mnemonic, units, value, descr)), + columns=['mnemonic', + 'unit', + 'value', + 'descr']) + + def plot_well_logs(self, + tracks: Union[str, list] = None, + depth_column: str = 'DEPTH', + depth_min: Union[int, float] = None, + depth_max: Union[int, float] = None, + colors: Union[str, list] = None, + add_well_tops: bool = False, + add_well_design: bool = False, + fill_between: int = None, + add_net_to_gross: int = None) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: + """Plot well logs. + + Parameters + __________ + + tracks : Union[str, list], default: ``None`` + Name/s of the logs to be plotted, e.g. ``tracks='SGR'`` or ``tracks=['SGR', 'K']``. + depth_column : str, default: ``'DEPTH'`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + depth_min : Union[int, float], default: ``None`` + Minimum depth to be plotted, e.g. ``depth_min=0``. + depth_max : Union[int, float], default: ``None`` + Maximum depth to be plotted, e.g. ``depth_max=2000``. + colors : Union[str, list], default: ``None`` + Colors of the logs, e.g. ``colors='black'`` or ``colors=['black', 'blue']``. + add_well_tops : bool, default: ``False`` + Boolean to add well tops to the plot, e.g. ``add_well_tops=True``. + add_well_design : bool, default: ``False`` + Boolean to add well design to the plot, e.g. ``add_well_design=True``. + fill_between : int, default: ``None`` + Number of the axis to fill, e.g. ``fill_between=0``. + add_net_to_gross : int, default: ``None`` + Number of axis to fill with the net to gross values, e.g. ``add_net_to_gross=0``. + + Returns + _______ + fig : matplotlib.figure.Figure + Matplotlib Figure. + ax : matplotlib.axes.Axes + Matplotlib Axes. + + Raises + ______ + TypeError + If the wrong input data types are provided. + ValueError + If no well tops are provided but ``add_well_tops`` is set to ``True``. + ValueError + If the wrong column names are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.plot_well_logs(tracks=['SGR', 'CAL'], depth_column='DEPTH', colors=['black', 'red']) + + See Also + ________ + plot_well_log_along_path : Plot well log along path. + calculate_vshale : Calculate Shale Volume. + calculate_vshale_linear : Calculate Shale Volume linear method. + calculate_net_to_gross : Calculate net to gross. + + .. versionadded:: 0.0.1 + """ + # Checking that the tracks are provided as list or string + if not isinstance(tracks, (list, str, type(None))): + raise TypeError('The track/s must bei either provided as str or a list of strings') + + # Checking that the tracks are in the DataFrame + if isinstance(tracks, str): + if not {tracks}.issubset(self.df.columns): + raise ValueError('The track is not part of the curves') + elif tracks is None: + tracks = list(self.df.columns) + tracks.remove('DEPTH') + elif isinstance(tracks, list): + if not all({track}.issubset(self.df.columns) for track in tracks): + raise ValueError('Not all tracks are part of the curves') + + # Checking that the depth column is of type string + if not isinstance(depth_column, str): + raise TypeError('Depth_column must be provided as string') + + # Checking that depth_min is of type float or int + if not isinstance(depth_min, (int, float, type(None))): + raise ValueError('The minimum depth must be provided as int or float') + + # Checking that depth_max is of type float or int + if not isinstance(depth_max, (int, float, type(None))): + raise ValueError('The maximum depth must be provided as int or float') + + # Checking that the depth column is in the DataFrame + if not {depth_column}.issubset(self.df): + raise ValueError('The depth_column is not part of the curves') + + # Checking that the colors are provided as list or string + if not isinstance(colors, (list, str, type(None))): + raise TypeError('The track/s must bei either provided as str or a list of strings') + + # Checking that the add_well_tops variable is of type bool + if not isinstance(add_well_tops, bool): + raise TypeError('The add_well_tops variable must be provided as bool') + + # Checking that the add_well_design variable is of type bool + if not isinstance(add_well_design, bool): + raise TypeError('The add_well_design variable must be provided as bool') + + # Checking that the fill_between variable is of type int + if not isinstance(fill_between, (int, type(None))): + raise TypeError('The fill_between variable must be provided as int') + + # Checking that the add_net to_gross variable is of type bool + if not isinstance(add_net_to_gross, (int, type(None))): + raise TypeError('The add_net_to_gross variable must be provided as int') + + # Selecting tracks + df = self.df[tracks + [depth_column]].reset_index() + + # Creating plot if tracks is of type string + if isinstance(tracks, str): + # Creating plot + fig, ax = plt.subplots(1, 1, figsize=(1 * 2, 8)) + + ax.plot(df[tracks], df[depth_column], color=colors) + ax.grid() + ax.invert_yaxis() + + if not depth_min: + depth_min = min(df[depth_column]) + if not depth_max: + depth_max = max(df[depth_column]) + + buffer = (depth_max - depth_min) / 20 + + ax.set_ylim(depth_max + buffer, depth_min - buffer) + ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + ax.xaxis.set_label_position('top') + ax.set_xlabel(tracks + ' [%s]' % + self.curves[self.curves['original_mnemonic'] == tracks].reset_index(drop=True)['unit'].iloc[ + 0], color='black') + ax.set_ylabel(depth_column + ' [m]') + + # Fill between curve + if fill_between: + left_col_value = np.min(df[tracks].dropna().values) + right_col_value = np.max(df[tracks].dropna().values) + span = abs(left_col_value - right_col_value) + cmap = plt.get_cmap('hot_r') + color_index = np.arange(left_col_value, right_col_value, span / 100) + # loop through each value in the color_index + for index in sorted(color_index): + index_value = (index - left_col_value) / span + color = cmap(index_value) # obtain color for color index value + ax.fill_betweenx(df[depth_column], df[tracks], left_col_value, where=df[tracks] >= index, + color=color) + + return fig, ax + + # Creating plot if tracks is of type list + elif isinstance(tracks, list): + + # Helping variable for adding well tops + if add_well_tops: + j = 1 + else: + j = 0 + + if add_well_design: + k = 1 + else: + k = 0 + + # Setting colors + if not colors: + colors = [None] * len(tracks) + + # Creating plot + fig, ax = plt.subplots(1, + len(tracks) + j + k, + figsize=((len(tracks) + j + k) * 1.8, 8), + sharey=True) + + # Adding well tops + if add_well_tops: + if not self.has_well_tops: + raise ValueError('Please add well tops to the borehole object to plot them') + else: + for index, row in self.well_tops.df.iterrows(): + ax[0 + k].axhline(row[self.well_tops.df.columns[1]], 0, 1, color='black') + ax[0 + k].text(0.05, row[self.well_tops.df.columns[1]] - 1, s=row[self.well_tops.df.columns[0]], + fontsize=6) + ax[0 + k].grid() + ax[0 + k].axes.get_xaxis().set_ticks([]) + + # Adding well design + if add_well_design: + if not self.has_well_design: + raise ValueError('Please add a well design to the borehole object to plot it') + else: + for key, elem in self.well_design.pipes.items(): + if elem.pipe_type == 'open hole section': + # Plotting open hole section as wavy line + y = np.linspace(elem.top, elem.bottom, 1000) + x1 = 0.5 * np.sin(y / 2) - elem.inner_diameter + 0.5 + x2 = 0.5 * np.cos(y / 2 + np.pi / 4) + elem.inner_diameter + ax[k].plot(x1, y, color='black') + ax[k].plot(x2, y, color='black') + # Plotting open hole section as dashed line + # ax.plot([elem.inner_diameter,elem.inner_diameter], + # [elem.top, elem.bottom], + # linestyle='--', + # color='black') + # ax.plot([-elem.inner_diameter, -elem.inner_diameter], + # [elem.top, elem.bottom], + # linestyle='--', + # color='black') + else: + ax[k].add_patch(Rectangle(elem.xy, elem.width, elem.height, color="black")) + ax[k].add_patch( + Rectangle((-1 * elem.xy[0], elem.xy[1]), -1 * elem.width, elem.height, color="black")) + + # Deep copy of pipes dict + x = copy.deepcopy(self.pipes) + + # Popping open hole section + try: + type_list = [elem.pipe_type for key, elem in x.items()] + index_open_hole = type_list.index('open hole section') + key_open_hole = list(x.keys())[index_open_hole] + x.pop(key_open_hole) + except ValueError: + pass + + # Getting diameters and calculating thickness of cement between each pipe + outer_diameters = sorted([elem.outer_diameter for key, elem in x.items()], reverse=False) + inner_diameters = sorted([elem.inner_diameter for key, elem in x.items()], reverse=False) + thicknesses = [y - x for x, y in zip(outer_diameters[:-1], inner_diameters[1:])] + + # Sorting pipes + pipes_sorted = {k: v for k, v in sorted(x.items(), key=lambda item: item[1].outer_diameter)} + + # Selecting pies + pipes_selected = {k: pipes_sorted[k] for k in list(pipes_sorted)[:len(thicknesses)]} + + # Plotting top of pipe + i = 0 + for key, elem in pipes_selected.items(): + i = i + ax[k].add_patch( + Rectangle((elem.inner_diameter, elem.top), thicknesses[i] + elem.thickness, 1, + color="black")) + ax[k].add_patch(Rectangle((-1 * elem.inner_diameter, elem.top), + -1 * thicknesses[0] - elem.thickness, 1, + color="black")) + i = i + 1 + + # Plotting Casing Shoes + for key, elem in self.well_design.pipes.items(): + if elem.shoe_width is not None: + p0 = [elem.outer_diameter, elem.bottom] + p1 = [elem.outer_diameter, elem.bottom - elem.shoe_height] + p2 = [elem.outer_diameter + elem.shoe_width, elem.bottom] + shoe = plt.Polygon([p0, p1, p2], color="black") + ax.add_patch(shoe) + p0[0] *= -1 + p1[0] *= -1 + p2 = [-elem.outer_diameter - elem.shoe_width, elem.bottom] + shoe = plt.Polygon([p0, p1, p2], color="black") + ax[k].add_patch(shoe) + + # Adding Casing Cements + for key, elem in self.well_design.cements.items(): + ax[k].fill_between(elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) + ax[k].fill_between(-1 * elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) + + # Calculating axes limits + top = np.min([elem.top for key, elem in self.well_design.pipes.items()]) + bottom = np.max([elem.bottom for key, elem in self.well_design.pipes.items()]) + max_diam = np.max([elem.outer_diameter for key, elem in self.well_design.pipes.items()]) + + # Setting axes limits + if not depth_min: + depth_min = min(df[depth_column]) + if not depth_max: + depth_max = max(df[depth_column]) + + buffer = (depth_max - depth_min) / 20 + + ax[k].set_ylim(depth_max + buffer, depth_min - buffer) + ax.set_xlim(-max_diam * 1, max_diam * 1) + # ax.invert_yaxis() + + # Setting axes labels + ax.set_xlabel('Diameter [in]') + + # Plotting tracks + for i in range(len(tracks)): + ax[i + j + k].plot(df[tracks[i]], df[depth_column], color=colors[i]) + ax[i + j + k].grid() + ax[i + j + k].invert_yaxis() + buffer = (max(df[depth_column]) - min(df[depth_column])) / 20 + ax[i + j + k].set_ylim(max(df[depth_column]) + buffer, min(df[depth_column]) - buffer) + ax[i + j + k].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + ax[i + j + k].xaxis.set_label_position('top') + ax[i + j + k].set_xlabel(tracks[i] + ' [%s]' % + self.curves[self.curves['original_mnemonic'] == tracks[i]].reset_index( + drop=True)[ + 'unit'].iloc[0], + color='black' if isinstance(colors[i], type(None)) else colors[i]) + ax[0].set_ylabel(depth_column + ' [m]') + + # Fill between curves + if fill_between is not None: + left_col_value = np.min(df[tracks[fill_between]].dropna().values) + right_col_value = np.max(df[tracks[fill_between]].dropna().values) + span = abs(left_col_value - right_col_value) + cmap = plt.get_cmap('hot_r') + color_index = np.arange(left_col_value, right_col_value, span / 100) + + # Dropping duplicate columns if the same track is selected twice + try: + df1 = df[tracks[fill_between]].copy(deep=True) + df1 = df1.loc[:, ~df1.columns.duplicated()] + except AttributeError: + df1 = df[tracks[fill_between]].copy(deep=True) + + # loop through each value in the color_index + for index in sorted(color_index): + index_value = (index - left_col_value) / span + color = cmap(index_value) # obtain color for color index value + ax[fill_between + j].fill_betweenx(df[depth_column], + # + df1.values.flatten(), + left_col_value, + where=df1.values.flatten() >= index, + color=color) + + # Adding net to gross + if add_net_to_gross is not None: + if 'N/G' not in self.df.columns: + raise ValueError('Net to gross has not been calculated yet') + + ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], + self.df[tracks[add_net_to_gross]], + 0, + where=self.df['N/G'] == 1, + color='yellow') + ax[add_net_to_gross + j].fill_betweenx(self.df[depth_column], + self.df[tracks[add_net_to_gross]], + 0, + where=self.df['N/G'] == 0, + color='brown') + + plt.tight_layout() + + return fig, ax + + def plot_well_log_along_path(self, + log: str, + depth_column: str = 'DEPTH', + relative: bool = True, + spacing: Union[float, int] = 0.5, + radius_factor: Union[float, int] = 75): + """Plot well log along path. + + Parameters + __________ + log : str + Name of the log, e.g. ``log='GR'``. + depth_column : str, default: ``'DEPTH'`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + relative : bool, default: ``True`` + Boolean value to plot the tube with relative coordinates, e.g. ``relative=False``. + spacing : Union[float, int], default: ``0.5`` + Spacing for the resampling of the well path, e.g. ``spacing=0.5``. + radius_factor : Union[float, int], default: ``75`` + Factor to scale the radius of the tube, e.g. ``radius_factor=75``. + + Return + ______ + tube : pv.core.pointset.PolyData + PyVista Tube of the borehole. + + Raises + ______ + ModuleNotFoundError + Raises error if PyVista is not installed. + TypeError + If the wrong input data types are provided. + ValueError + Raises error if the wrong column names are provided. + ValueError + Raises error if no ``Deviation`` has been defined. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.plot_well_log_along_path(log='SGR', depth_column='DEPTH', spacing=0.5, radius_factor=10) + + =========== ========================= + PolyData Information + =========== ========================= + N Cells 22 + N Points 40040 + N Strips 22 + X Bounds 3.413e+06, 3.413e+06 + Y Bounds 5.836e+06, 5.836e+06 + Z Bounds -9.925e+01, -5.000e-02 + N Arrays 2 + =========== ========================= + + ============ ======= ======== ======= =========== ========== + Name Field Type N Comp Min Max + ============ ======= ======== ======= =========== ========== + values Points float64 1 4.342e+00 1.474e+02 + TubeNormals Points float32 3 -1.000e+00 1.000e+00 + ============ ======= ======== ======= =========== ========== + + See Also + ________ + plot_well_logs : Plot well logs. + + .. versionadded:: 0.0.1 + """ + # Importing pyvista + try: + import pyvista as pv + except ModuleNotFoundError: + ModuleNotFoundError('PyVista package not installed') + + # Checking that log is provided as string + if not isinstance(log, str): + raise TypeError('The name of the log must be provided as str') + + # Checking that the depth column is of type string + if not isinstance(depth_column, str): + raise TypeError('Depth_column must be provided as string') + + # Checking that the relative value is provided as bool + if not isinstance(relative, bool): + raise TypeError('The relative value must be provided as bool') + + # Checking that the spacing is of type float or int + if not isinstance(spacing, (float, int)): + raise TypeError('The spacing for the resampling must be provided as float or int') + + # Checking that the radius_factor is of type float or int + if not isinstance(radius_factor, (float, int)): + raise TypeError('The radius_factor must be provided as float or int') + + # Checking if the borehole deviation is present + if not self._borehole.deviation: + raise ValueError('Borehole deviation is needed to create a tube') + + # Extracting the coordinates + coordinates = self._borehole.deviation.desurveyed_df.copy(deep=True) + + if relative: + if not {'Northing_rel', 'Easting_rel', 'True Vertical Depth'}.issubset(coordinates.columns): + raise ValueError( + 'The coordinates DataFrame must contain Northing_rel, Easting_rel and True Vertical Depth ' + 'below sea level column') + + coordinates['True Vertical Depth'] = coordinates['True Vertical Depth'] * (-1) + xyz = coordinates[['Easting_rel', 'Northing_rel', 'True Vertical Depth']].to_numpy() + + else: + if not {'Northing', 'Easting', 'True Vertical Depth Below Sea Level'}.issubset(coordinates.columns): + raise ValueError( + 'The coordinates DataFrame must contain Northing, Easting and True Vertical Depth Below Sea Level ' + 'below sea level column') + + coordinates['True Vertical Depth Below Sea Level'] = coordinates['True Vertical Depth Below Sea Level'] * ( + -1) + xyz = coordinates[['Easting', 'Northing', 'True Vertical Depth Below Sea Level']].to_numpy() + + # Obtaining log values + logs = self.df.reset_index()[[depth_column, log]] + + # Resample points + points = resample_between_well_deviation_points(coordinates=xyz, + spacing=spacing) + + # Creating spline + polyline_well_path_resampled = pv.Spline(points) + + # Getting points along the spline + points_along_spline = get_points_along_spline(spline=polyline_well_path_resampled, + dist=logs[depth_column].values) + + # Create polyline from points + polyline_along_spline = polyline_from_points(points=points_along_spline) + + # Assign log values to polyline + polyline_along_spline['values'] = logs[log].values + + # Create tube from polyline + tube_along_spline = polyline_along_spline.tube(scalars='values', + radius_factor=radius_factor) + + return tube_along_spline + + def calculate_vshale(self, + method: str, + column: str, + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None) -> pd.DataFrame: + """Calculate Shale Volume. + + Parameters + __________ + method : str + Method used to calculate the Shale Volume, e.g. ``method='linear'``. + The following methods are available + + ==================== ======================================= + ``linear`` Linear Gamma Ray Index Shale Volume + ``larionov_old`` Larionov Shale volume for old rocks + ``larionov_young`` Larionov Shale volume for young rocks + ``clavier`` Clavier Shale volume + ``stieber`` Stieber Shale volume + ==================== ======================================= + + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Returns + _______ + borehole.logs.df : pd.DataFrame + Log DataFrame with appended Shale Volume. + + ============= ========================== + Index Index of each measurement + ---- Remaining columns + Vshale_____ Shale volume + ============= ========================== + + Raises + ______ + TypeError + If the wrong input data types are provided. + ValueError + If a method is chosen that is not available. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_vshale(method='linear', column='GR') + >>> borehole.logs.df + + ======= ====== ==== ============== + Index DEPTH --- VShale_Linear + ======= ====== ==== ============== + 0 0.05 --- 0.037104 + 1 0.10 --- 0.043358 + 2 0.15 --- 0.049788 + 3 0.20 --- 0.055837 + ======= ====== ==== ============== + + See Also + ________ + calculate_vshale_linear : Calculate Shale Volume with the linear method. + calculate_net_to_gross : Calculate Net to Gross value. + + References + __________ + - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. + - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. + - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. + + .. versionadded:: 0.0.1 + """ + # Checking that the method is of type str + if not isinstance(method, str): + raise TypeError('The method must be provided as string') + + # Checking that the provided method is in the list of methods + if method not in ['linear']: + raise ValueError('Provided method is not implemented') + + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Selecting method + if method == 'linear': + self.calculate_vshale_linear(column=column, + minz=minz, + maxz=maxz, + depth_column=depth_column) + + return self.df + + def calculate_vshale_linear(self, + column: str, + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None): + """Calculate Shale Volume with the linear method. + + Parameters + __________ + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_vshale_linear(column='GR') + >>> borehole.logs.df + + ======= ====== ==== ============== + Index DEPTH --- VShale_Linear + ======= ====== ==== ============== + 0 0.05 --- 0.037104 + 1 0.10 --- 0.043358 + 2 0.15 --- 0.049788 + 3 0.20 --- 0.055837 + ======= ====== ==== ============== + + See Also + ________ + calculate_vshale : Calculate Shale Volume. + calculate_net_to_gross : Calculate Net to Gross value. + + .. versionadded:: 0.0.1 + """ + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Checking that the depth column is of type string + if not isinstance(depth_column, (str, type(None))): + raise TypeError('Depth_column must be provided as string') + + # Cropping DataFrame + if None not in (minz, maxz): + df = self.df[(self.df[depth_column] >= minz) & (self.df[depth_column] <= maxz)].copy(deep=True) + else: + df = self.df.copy(deep=True) + + # Obtaining min and max Gamma Ray values + gr_max = df[column].max() + gr_min = df[column].min() + + # Calculating Shale Volume + vshale = (df[column] - gr_min) / (gr_max - gr_min) + + # Appending Shale Volume to Borehole Logs DataFrame + self.df['VShale_Linear'] = vshale + + def calculate_net_to_gross(self, + method: str, + column: str, + cutoff: Union[float, int], + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None) -> float: + """Calculate Net to Gross value. + + Parameters + __________ + method : str + Method used to calculate the Shale Volume, e.g. ``method='linear'``. + + ==================== ======================================= + ``linear`` Linear Gamma Ray Index Shale Volume + ``larionov_old`` Larionov Shale volume for old rocks + ``larionov_young`` Larionov Shale volume for young rocks + ``clavier`` Clavier Shale volume + ``stieber`` Stieber Shale volume + ==================== ======================================= + + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + cutoff : Union[float, int] + Cutoff value for net to gross estimation, e.g. ``cutoff=0.3``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Returns + _______ + net_to_gross : float + Net to gross value. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_net_to_gross(method='linear', column='GR', cutoff=0.3) + 0.5 + + See Also + ________ + calculate_vshale : Calculate Shale Volume. + calculate_vshale_linear : Calculate Shale Volume with the linear method. + + References + __________ + - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. + - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. + - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. + + .. versionadded:: 0.0.1 + """ + # Checking that the method is of type str + if not isinstance(method, str): + raise TypeError('The method must be provided as string') + + # Checking that the provided method is in the list of methods + if method not in ['linear']: + raise ValueError('Provided method is not implemented') + + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the cutoff value is of type float or int + if not isinstance(cutoff, (float, int)): + raise TypeError('The cutoff value must be provided as float or int') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Checking that the depth column is of type string + if not isinstance(depth_column, (str, type(None))): + raise TypeError('Depth_column must be provided as string') + + # Calculating Shale Volume + self.calculate_vshale(method=method, + column=column, + minz=minz, + maxz=maxz, + depth_column=depth_column) + + # Setting column name of Shale Volume + if method == 'linear': + column_vshale = 'VShale_Linear' + + # Assigning net to gross value for each row + def calculating_ng(row, + cutoff_value): + if row <= cutoff_value: + return 1 + elif cutoff_value < row <= 1: + return 0 + else: + return -1 + + # Calculating Net to Gross for each Shale Volume value + self.df['N/G'] = self.df[column_vshale].apply(lambda row: calculating_ng(row, cutoff)) + + # Calculating Net to Gross value + net_to_gross = self.df['N/G'].value_counts()[0] / ( + self.df['N/G'].value_counts()[0] + self.df['N/G'].value_counts()[1]) + + return net_to_gross diff --git a/pyborehole/logs_las.py b/pyborehole/logs_las.py new file mode 100644 index 0000000..9a3fce1 --- /dev/null +++ b/pyborehole/logs_las.py @@ -0,0 +1,1125 @@ +from typing import Union, Tuple +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Rectangle +import matplotlib +import copy + +from pyborehole.plot import plot_well_logs as pwl + + +class LASLogs: + """Class to initiate a Well Log Object. + + Parameters + __________ + borehole : Borehole + Borehole object. + path : str + Path to the well logs, e.g. ``path='logs.las'``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the values of all curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= ======== ======= ======= ======== + Index DEPTH SGR TH U K + ======= ======= ======== ======= ======= ======== + 0 0.05 9.6514 1.8866 0.4018 0.2414 + 1 0.10 10.5465 2.1613 0.4206 0.2562 + 2 0.15 11.4666 2.4533 0.4397 0.2698 + 3 0.20 12.3323 2.7352 0.4601 0.2800 + ======= ======= ======== ======= ======= ======== + + curves : pd.DataFrame + Pandas DataFrame providing information about the measured curves. Data columns are as follows: + + =================== ============================================== + Index Index of each curve + original_mnemonic Origial name of a curve in the well log file + mnemonic Name of a curve in the well log file + descr Description of a curve in the well log file + unit Unit of a curve in the well log file + =================== ============================================== + + **Example:** + + ====== ================== ========= ======================================= ===== + Index original_mnemonic mnemonic descr unit + 0 DEPT DEPT Borehole Depth M + 1 SGR SGR Gamma Ray derived from Spectrum gAPI + 2 TH TH Thorium content from Spectrum Gamma ppm + 3 U U Uranium content from Spectrum Gamma ppm + 4 K K Potassium content from Spectrum Gamma %wt + ====== ================== ========= ======================================= ===== + + well_header : pd.DataFrame + Pandas DataFrame providing information about the well. Data columns are as follows: + + ========== ===================================== + Index Index of each well information + mmnemonic Name of the well information + unit Unit of the well information + value Value of the well information + descr Description of the well information + ========== ===================================== + + **Example:** + + ====== ========= ===== =============== =================== + Index mnemonic unit value descr + ====== ========= ===== =============== =================== + 0 STRT M 100.0 Log Start Depth + 1 STOP M 0.05 Log Stop Depth + 2 STEP M -0.05 Log Increment + 3 NULL -999.25 Null Value + 4 COMP RWE Power Company Name + 5 WELL EB 1 Well Name + 6 FLD KW Weisweiler Field Name + 7 LOC Location + 8 PROV Province + 9 SRVC Service Company + 10 DATE 26-Oct-2023 Date + 11 UWI Unique Well ID + ====== ========= ===== =============== =================== + + params : pd.DataFrame + Pandas DataFrame containing information about the well. Data columns are as follows: + + ========== ===================================== + Index Index of each well information + mmnemonic Name of the well information + unit Unit of the well information + value Value of the well information + descr Description of the well information + ========== ===================================== + + **Example:** + + ======= ========== ================ ================= + Index mnemonic unit value descr + ======= ========== ================ ================= + 0 BHT 20 degC BHT + 1 CVL 5350 m/s CasingVelocity + 2 CTRY NRW Country + 3 FLVT 1500 m/s FluidVelocity + 4 LMFI 0.6 G MFieldIntensity + 5 MUDD 1.0 g/ccm MudWeight + 6 RECORDED Mustermann Engineer's Name + 7 RUN 1 Run Number + 8 TIME 20:59:35 Time + 9 WITTNESS Herr Mustermann Witness's Name + ======= ========== ================ ================= + + + Returns + _______ + LASLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.well_header + + ======== ========= ====== ================ =================== + Index mnemonic unit value descr + ======== ========= ====== ================ =================== + 0 STRT M 100.0 Log Start Depth + 1 STOP M 0.05 Log Stop Depth + 2 STEP M -0.05 Log Increment + 3 NULL -999.25 Null Value + 4 COMP RWE Power Company Name + 5 WELL EB 1 Well Name + 6 FLD KW Weisweiler Field Name + 7 LOC Location + 8 PROV Province + 9 SRVC Service Company + 10 DATE 26-Oct-2023 Date + 11 UWI Unique Well ID + ======== ========= ====== ================ =================== + + See Also + ________ + pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. + pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. + pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. + pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, + borehole, + path: str): + """Class to initiate a Well Log Object. + + Parameters + __________ + borehole : Borehole + Borehole object. + path : str + Path to the well logs, e.g. ``path='logs.las'``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the values of all curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= ======== ======= ======= ======== + Index DEPTH SGR TH U K + ======= ======= ======== ======= ======= ======== + 0 0.05 9.6514 1.8866 0.4018 0.2414 + 1 0.10 10.5465 2.1613 0.4206 0.2562 + 2 0.15 11.4666 2.4533 0.4397 0.2698 + 3 0.20 12.3323 2.7352 0.4601 0.2800 + ======= ======= ======== ======= ======= ======== + + curves : pd.DataFrame + Pandas DataFrame providing information about the measured curves. Data columns are as follows: + + =================== ============================================== + Index Index of each curve + original_mnemonic Origial name of a curve in the well log file + mnemonic Name of a curve in the well log file + descr Description of a curve in the well log file + unit Unit of a curve in the well log file + =================== ============================================== + + **Example:** + + ====== ================== ========= ======================================= ===== + Index original_mnemonic mnemonic descr unit + 0 DEPT DEPT Borehole Depth M + 1 SGR SGR Gamma Ray derived from Spectrum gAPI + 2 TH TH Thorium content from Spectrum Gamma ppm + 3 U U Uranium content from Spectrum Gamma ppm + 4 K K Potassium content from Spectrum Gamma %wt + ====== ================== ========= ======================================= ===== + + well_header : pd.DataFrame + Pandas DataFrame providing information about the well. Data columns are as follows: + + ========== ===================================== + Index Index of each well information + mmnemonic Name of the well information + unit Unit of the well information + value Value of the well information + descr Description of the well information + ========== ===================================== + + **Example:** + + ====== ========= ===== =============== =================== + Index mnemonic unit value descr + ====== ========= ===== =============== =================== + 0 STRT M 100.0 Log Start Depth + 1 STOP M 0.05 Log Stop Depth + 2 STEP M -0.05 Log Increment + 3 NULL -999.25 Null Value + 4 COMP RWE Power Company Name + 5 WELL EB 1 Well Name + 6 FLD KW Weisweiler Field Name + 7 LOC Location + 8 PROV Province + 9 SRVC Service Company + 10 DATE 26-Oct-2023 Date + 11 UWI Unique Well ID + ====== ========= ===== =============== =================== + + params : pd.DataFrame + Pandas DataFrame containing information about the well. Data columns are as follows: + + ========== ===================================== + Index Index of each well information + mmnemonic Name of the well information + unit Unit of the well information + value Value of the well information + descr Description of the well information + ========== ===================================== + + **Example:** + + ======= ========== ================ ================= + Index mnemonic unit value descr + ======= ========== ================ ================= + 0 BHT 20 degC BHT + 1 CVL 5350 m/s CasingVelocity + 2 CTRY NRW Country + 3 FLVT 1500 m/s FluidVelocity + 4 LMFI 0.6 G MFieldIntensity + 5 MUDD 1.0 g/ccm MudWeight + 6 RECORDED Mustermann Engineer's Name + 7 RUN 1 Run Number + 8 TIME 20:59:35 Time + 9 WITTNESS Herr Mustermann Witness's Name + ======= ========== ================ ================= + + Returns + _______ + LASLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.well_header + + ======== ========= ====== ================ =================== + Index mnemonic unit value descr + ======== ========= ====== ================ =================== + 0 STRT M 100.0 Log Start Depth + 1 STOP M 0.05 Log Stop Depth + 2 STEP M -0.05 Log Increment + 3 NULL -999.25 Null Value + 4 COMP RWE Power Company Name + 5 WELL EB 1 Well Name + 6 FLD KW Weisweiler Field Name + 7 LOC Location + 8 PROV Province + 9 SRVC Service Company + 10 DATE 26-Oct-2023 Date + 11 UWI Unique Well ID + ======== ========= ====== ================ =================== + + .. versionadded:: 0.0.1 + """ + # Importing lasio + try: + import lasio + except ModuleNotFoundError: + ModuleNotFoundError('lasio package not installed') + + # Checking that the path is provided as string + if not isinstance(path, str): + raise TypeError('The path must be provided as string') + + # Opening LAS file + las = lasio.read(path) + + # Extracting DataFrame from LAS file + self.df = las.df().sort_index() + + # Changing the name of the index column and reset index + self.df.index.rename('DEPTH', inplace=True) + self.df = self.df.reset_index() + + # Creating DataFrame from curve data + self.curves = pd.DataFrame(list(zip([las.curves[i]['original_mnemonic'] for i in range(len(las.curves))], + [las.curves[i]['mnemonic'] for i in range(len(las.curves))], + [las.curves[i]['descr'] for i in range(len(las.curves))], + [las.curves[i]['unit'] for i in range(len(las.curves))])), + columns=['original_mnemonic', + 'mnemonic', + 'descr', + 'unit']) + + # Changing name of depth column in curves DataFrame + self.curves.at[0, 'original_mnemonic'] = 'DEPTH' + self.curves.at[0, 'mnemonic'] = 'DEPTH' + + # Creating DataFrame from well header + self.well_header = pd.DataFrame(list(zip([las.well[i]['mnemonic'] for i in range(len(las.well))], + [las.well[i]['unit'] for i in range(len(las.well))], + [las.well[i]['value'] for i in range(len(las.well))], + [las.well[i]['descr'] for i in range(len(las.well))])), + columns=['mnemonic', + 'unit', + 'value', + 'descr']) + + # Creating DataFrame from parameters + self.params = pd.DataFrame(list(zip([las.params[i]['mnemonic'] for i in range(len(las.params))], + [las.params[i]['unit'] for i in range(len(las.params))], + [las.params[i]['value'] for i in range(len(las.params))], + [las.params[i]['descr'] for i in range(len(las.params))])), + columns=['mnemonic', + 'unit', + 'value', + 'descr']) + + # Assigning attributes + self.well_tops = borehole.well_tops + self.has_well_tops = borehole.has_well_tops + self.has_well_design = borehole.has_well_design + self.well_design = borehole.well_design + self._borehole = borehole + + def plot_well_logs(self, + tracks: Union[str, list] = None, + depth_column: str = 'DEPTH', + depth_min: Union[int, float] = None, + depth_max: Union[int, float] = None, + colors: Union[str, list] = None, + add_well_tops: bool = False, + add_well_design: bool = False, + fill_between: int = None, + add_net_to_gross: int = None, + fontsize_well_tops: Union[int, float] = 12, + use_df_tdom: bool = False) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: + """Plot well logs. + + Parameters + __________ + + tracks : Union[str, list] + Name/s of the logs to be plotted, e.g. ``tracks='SGR'`` or ``tracks=['SGR', 'K']``. + depth_column : str, default: ``'DEPTH'`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + depth_min : Union[int, float] + Minimum depth to be plotted, e.g. ``depth_min=0``. + depth_max : Union[int, float] + Maximum depth to be plotted, e.g. ``depth_max=2000``. + colors : Union[str, list], default: ``None`` + Colors of the logs, e.g. ``colors='black'`` or ``colors=['black', 'blue']``. + add_well_tops : bool, default: ``False`` + Boolean to add well tops to the plot, e.g. ``add_well_tops=True``. + add_well_design : bool, default: ``False`` + Boolean to add well design to the plot, e.g. ``add_well_design=True``. + fill_between : int, default: ``None`` + Number of the axis to fill, e.g. ``fill_between=0``. + add_net_to_gross : int, default: ``None`` + Number of axis to fill with the net to gross values, e.g. ``add_net_to_gross=0``. + + Returns + _______ + fig : matplotlib.figure.Figure + Matplotlib Figure. + ax : matplotlib.axes.Axes + Matplotlib Axes. + + Raises + ______ + TypeError + If the wrong input data types are provided. + ValueError + If no well tops are provided but ``add_well_tops`` is set to ``True``. + ValueError + If the wrong column names are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.plot_well_logs(tracks=['SGR', 'CAL'], depth_column='DEPTH', colors=['black', 'red']) + + See Also + ________ + plot_well_log_along_path : Plot well log along path. + calculate_vshale : Calculate Shale Volume. + calculate_vshale_linear : Calculate Shale Volume linear method. + calculate_net_to_gross : Calculate net to gross. + + .. versionadded:: 0.0.1 + """ + + fig, ax = pwl(self, + tracks=tracks, + depth_column=depth_column, + depth_min=depth_min, + depth_max=depth_max, + colors=colors, + add_well_tops=add_well_tops, + add_well_design=add_well_design, + fill_between=fill_between, + add_net_to_gross=add_net_to_gross, + fontsize_well_tops=fontsize_well_tops, + use_df_tdom=use_df_tdom) + + return fig, ax + + + def plot_well_log_along_path(self, + log: str, + depth_column: str = 'DEPTH', + relative: bool = True, + spacing: Union[float, int] = 0.5, + radius_factor: Union[float, int] = 75): + """Plot well log along path. + + Parameters + __________ + log : str + Name of the log, e.g. ``log='GR'``. + depth_column : str, default: ``'DEPTH'`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + relative : bool, default: ``True`` + Boolean value to plot the tube with relative coordinates, e.g. ``relative=False``. + spacing : Union[float, int], default: ``0.5`` + Spacing for the resampling of the well path, e.g. ``spacing=0.5``. + radius_factor : Union[float, int], default: ``75`` + Factor to scale the radius of the tube, e.g. ``radius_factor=75``. + + Return + ______ + tube : pv.core.pointset.PolyData + PyVista Tube of the borehole. + + Raises + ______ + ModuleNotFoundError + Raises error if PyVista is not installed. + TypeError + If the wrong input data types are provided. + ValueError + Raises error if the wrong column names are provided. + ValueError + Raises error if no ``Deviation`` has been defined. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.plot_well_log_along_path(log='SGR', depth_column='DEPTH', spacing=0.5, radius_factor=10) + + =========== ========================= + PolyData Information + =========== ========================= + N Cells 22 + N Points 40040 + N Strips 22 + X Bounds 3.413e+06, 3.413e+06 + Y Bounds 5.836e+06, 5.836e+06 + Z Bounds -9.925e+01, -5.000e-02 + N Arrays 2 + =========== ========================= + + ============ ======= ======== ======= =========== ========== + Name Field Type N Comp Min Max + ============ ======= ======== ======= =========== ========== + values Points float64 1 4.342e+00 1.474e+02 + TubeNormals Points float32 3 -1.000e+00 1.000e+00 + ============ ======= ======== ======= =========== ========== + + See Also + ________ + plot_well_logs : Plot well logs. + + .. versionadded:: 0.0.1 + """ + # Importing pyvista + try: + import pyvista as pv + except ModuleNotFoundError: + ModuleNotFoundError('PyVista package not installed') + + # Checking that log is provided as string + if not isinstance(log, str): + raise TypeError('The name of the log must be provided as str') + + # Checking that the depth column is of type string + if not isinstance(depth_column, str): + raise TypeError('Depth_column must be provided as string') + + # Checking that the relative value is provided as bool + if not isinstance(relative, bool): + raise TypeError('The relative value must be provided as bool') + + # Checking that the spacing is of type float or int + if not isinstance(spacing, (float, int)): + raise TypeError('The spacing for the resampling must be provided as float or int') + + # Checking that the radius_factor is of type float or int + if not isinstance(radius_factor, (float, int)): + raise TypeError('The radius_factor must be provided as float or int') + + # Checking if the borehole deviation is present + if not self._borehole.deviation: + raise ValueError('Borehole deviation is needed to create a tube') + + # Extracting the coordinates + coordinates = self._borehole.deviation.desurveyed_df.copy(deep=True) + + if relative: + if not {'Northing_rel', 'Easting_rel', 'True Vertical Depth'}.issubset(coordinates.columns): + raise ValueError( + 'The coordinates DataFrame must contain Northing_rel, Easting_rel and True Vertical Depth ' + 'below sea level column') + + coordinates['True Vertical Depth'] = coordinates['True Vertical Depth'] * (-1) + xyz = coordinates[['Easting_rel', 'Northing_rel', 'True Vertical Depth']].to_numpy() + + else: + if not {'Northing', 'Easting', 'True Vertical Depth Below Sea Level'}.issubset(coordinates.columns): + raise ValueError( + 'The coordinates DataFrame must contain Northing, Easting and True Vertical Depth Below Sea Level ' + 'below sea level column') + + coordinates['True Vertical Depth Below Sea Level'] = coordinates['True Vertical Depth Below Sea Level'] * ( + -1) + xyz = coordinates[['Easting', 'Northing', 'True Vertical Depth Below Sea Level']].to_numpy() + + # Obtaining log values + logs = self.df.reset_index()[[depth_column, log]] + + # Resample points + points = resample_between_well_deviation_points(coordinates=xyz, + spacing=spacing) + + # Creating spline + polyline_well_path_resampled = pv.Spline(points) + + # Getting points along the spline + points_along_spline = get_points_along_spline(spline=polyline_well_path_resampled, + dist=logs[depth_column].values) + + # Create polyline from points + polyline_along_spline = polyline_from_points(points=points_along_spline) + + # Assign log values to polyline + polyline_along_spline['values'] = logs[log].values + + # Create tube from polyline + tube_along_spline = polyline_along_spline.tube(scalars='values', + radius_factor=radius_factor) + + return tube_along_spline + + def calculate_vshale(self, + method: str, + column: str, + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None) -> pd.DataFrame: + """Calculate Shale Volume. + + Parameters + __________ + method : str + Method used to calculate the Shale Volume, e.g. ``method='linear'``. Other methods include: + + ==================== ======================================= + ``linear`` Linear Gamma Ray Index Shale Volume + ``larionov_old`` Larionov Shale volume for old rocks + ``larionov_young`` Larionov Shale volume for young rocks + ``clavier`` Clavier Shale volume + ``stieber`` Stieber Shale volume + ==================== ======================================= + + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Returns + _______ + borehole.logs.df : pd.DataFrame + Log DataFrame with appended Shale Volume. + + ============= ========================== + Index Index of each measurement + ---- Remaining columns + Vshale_____ Shale volume + ============= ========================== + + Raises + ______ + TypeError + If the wrong input data types are provided. + ValueError + If a method is chosen that is not available. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_vshale(method='linear', column='GR') + >>> borehole.logs.df + + ======= ====== ==== ============== + Index DEPTH --- VShale_Linear + ======= ====== ==== ============== + 0 0.05 --- 0.037104 + 1 0.10 --- 0.043358 + 2 0.15 --- 0.049788 + 3 0.20 --- 0.055837 + ======= ====== ==== ============== + + See Also + ________ + calculate_vshale_linear : Calculate Shale Volume with the linear method. + calculate_net_to_gross : Calculate Net to Gross value. + + References + __________ + - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. + - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. + - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. + + .. versionadded:: 0.0.1 + """ + # Checking that the method is of type str + if not isinstance(method, str): + raise TypeError('The method must be provided as string') + + # Checking that the provided method is in the list of methods + if method not in ['linear']: + raise ValueError('Provided method is not implemented') + + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Selecting method + if method == 'linear': + self.calculate_vshale_linear(column=column, + minz=minz, + maxz=maxz, + depth_column=depth_column) + + return self.df + + def calculate_vshale_linear(self, + column: str, + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None): + """Calculate Shale Volume with the linear method. + + Parameters + __________ + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_vshale_linear(column='GR') + >>> borehole.logs.df + + ======= ====== ==== ============== + Index DEPTH --- VShale_Linear + ======= ====== ==== ============== + 0 0.05 --- 0.037104 + 1 0.10 --- 0.043358 + 2 0.15 --- 0.049788 + 3 0.20 --- 0.055837 + ======= ====== ==== ============== + + See Also + ________ + calculate_vshale : Calculate Shale Volume. + calculate_net_to_gross : Calculate Net to Gross value. + + .. versionadded:: 0.0.1 + """ + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Checking that the depth column is of type string + if not isinstance(depth_column, (str, type(None))): + raise TypeError('Depth_column must be provided as string') + + # Cropping DataFrame + if None not in (minz, maxz): + df = self.df[(self.df[depth_column] >= minz) & (self.df[depth_column] <= maxz)].copy(deep=True) + else: + df = self.df.copy(deep=True) + + # Obtaining min and max Gamma Ray values + gr_max = df[column].max() + gr_min = df[column].min() + + # Calculating Shale Volume + vshale = (df[column] - gr_min) / (gr_max - gr_min) + + # Appending Shale Volume to Borehole Logs DataFrame + self.df['VShale_Linear'] = vshale + + def calculate_net_to_gross(self, + method: str, + column: str, + cutoff: Union[float, int], + minz: Union[float, int] = None, + maxz: Union[float, int] = None, + depth_column: str = None) -> float: + """Calculate Net to Gross value. + + Parameters + __________ + method : str + Method used to calculate the Shale Volume, e.g. ``method='linear'``. Other methods include: + + ==================== ======================================= + ``linear`` Linear Gamma Ray Index Shale Volume + ``larionov_old`` Larionov Shale volume for old rocks + ``larionov_young`` Larionov Shale volume for young rocks + ``clavier`` Clavier Shale volume + ``stieber`` Stieber Shale volume + ==================== ======================================= + + column : str + Column of the borehole.logs.df containing the Gamma Ray Value, e.g. ``column='GR'``. + cutoff : Union[float, int] + Cutoff value for net to gross estimation, e.g. ``cutoff=0.3``. + minz : Union[float, int], default: ``None`` + Minimum Z value, e.g. ``minz=50``. + maxz : Union[float, int], default: ``None`` + Maximum Z value, e.g. ``maxz=100``. + depth_column : str, default: ``None`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + + Returns + _______ + net_to_gross : float + Net to gross value. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs('Well_Logs.las') + >>> borehole.logs.calculate_net_to_gross(method='linear', column='GR', cutoff=0.3) + 0.5 + + See Also + ________ + calculate_vshale : Calculate Shale Volume. + calculate_vshale_linear : Calculate Shale Volume with the linear method. + + References + __________ + - Larionov, V.V. (1969) Borehole Radiometry Moscow, U.S.S.R. In: Nedra, M.R.L. and Biggs, W.P., Eds., Using Log-Derived Values of Water Saturation and Porosity, Trans. SPWLA Ann. Logging Symp. Paper, 10, 26. + - Stieber, S.J. (1970) Pulsed Neutron Capture Log Evaluation - Louisiana Gulf Coast. Paper presented at the Fall Meeting of the Society of Petroleum Engineers of AIME, Houston, Texas, October, https://doi.org/10.2118/2961-MS. + - Clavier, C., Hoyle, W., and Meunier, D. (1971) Quantitative Interpretation of Thermal Neutron Decay Time Logs: Part I. Fundamentals and Techniques. J Pet Technol 23 (1971): 743–755, https://doi.org/10.2118/2658-A-PA. + + .. versionadded:: 0.0.1 + """ + # Checking that the method is of type str + if not isinstance(method, str): + raise TypeError('The method must be provided as string') + + # Checking that the provided method is in the list of methods + if method not in ['linear']: + raise ValueError('Provided method is not implemented') + + # Checking that the column is of type str + if not isinstance(column, str): + raise TypeError('The column name must be provided as string') + + # Checking that the cutoff value is of type float or int + if not isinstance(cutoff, (float, int)): + raise TypeError('The cutoff value must be provided as float or int') + + # Checking that the minz value is of type float or int + if not isinstance(minz, (float, int, type(None))): + raise TypeError('minz must be provided as float or int') + + # Checking that the maxz value is of type float or int + if not isinstance(maxz, (float, int, type(None))): + raise TypeError('maxz must be provided as float or int') + + # Checking that the depth column is of type string + if not isinstance(depth_column, (str, type(None))): + raise TypeError('Depth_column must be provided as string') + + # Calculating Shale Volume + self.calculate_vshale(method=method, + column=column, + minz=minz, + maxz=maxz, + depth_column=depth_column) + + # Setting column name of Shale Volume + if method == 'linear': + column_vshale = 'VShale_Linear' + + # Assigning net to gross value for each row + def calculating_ng(row, + cutoff_value): + if row <= cutoff_value: + return 1 + elif cutoff_value < row <= 1: + return 0 + else: + return -1 + + # Calculating Net to Gross for each Shale Volume value + self.df['N/G'] = self.df[column_vshale].apply(lambda row: calculating_ng(row, cutoff)) + + # Calculating Net to Gross value + net_to_gross = self.df['N/G'].value_counts()[0] / ( + self.df['N/G'].value_counts()[0] + self.df['N/G'].value_counts()[1]) + + return net_to_gross + + def add_curve_to_curve(self, + original_mnemonic: str, + mnemonic: str, + descr: str, + unit: str): + """Add curve to curves DataFrame so it can also be used for plotting. + + Parameters + __________ + original_mnemonic : str + Original mnemonic of the track. + mnemonic : str + Mnemonic of the track. + descr : str + Description of the track. + unit : str + Unit of the data of the track. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> borehole.logs.add_curve_to_curves(original_mnemonic='Velocity', mnemonic='Velocity', descr='Velocity', unit='m/s') + >>> borehole.logs.curves + + .. versionadded:: 0.0.1 + """ + + # Checking that the original mnemonic is provided as string + if not isinstance(original_mnemonic, str): + raise TypeError('original_mnemonic must be provided as string') + + # Checking that the mnemonic is provided as string + if not isinstance(mnemonic, str): + raise TypeError('mnemonic must be provided as string') + + # Checking that the description is provided as string + if not isinstance(descr, str): + raise TypeError('descr must be provided as string') + + # Checking that the unit is provided as string + if not isinstance(unit, str): + raise TypeError('unit must be provided as string') + + # Checking that track is not available in the curves DataFrame + if original_mnemonic in self.curves: + raise ValueError('Track is already present in Curve DataFrame') + + # Concatenate new curve to existing curves + self.curves = pd.concat([self.curves, + pd.DataFrame.from_dict( + {'original_mnemonic': [original_mnemonic], + 'mnemonic': [mnemonic], + 'descr': [descr], + 'unit': [unit]}), + ], + ignore_index=True) + + def despike_curve(self, + track: str, + window_length: int): + + """Despike curve using a rolling window. + + + """ + + self.df[track + '_rolling'] = self.df.rolling(window=window_length)[track].mean() + + curve_df = self.curves[self.curves['original_mnemonic'] == track].reset_index(drop=True).iloc[0] + + self.add_curve_to_curve(original_mnemonic=track + '_rolling', + mnemonic=curve_df['mnemonic'] + '_rolling', + descr=curve_df['descr'] + '_rolling', + unit=curve_df['unit']) + + def calculate_time_depth_relationship(self, + track: str, + depth_column: str = 'DEPTH', + replacement_velocity: Union[int, float] = 2000): + log_start = self.df[depth_column].iloc[0] + altitude = self._borehole.altitude_above_sea_level + gap_int = log_start - altitude + log_start_time = 2 * gap_int / replacement_velocity + + increment = \ + self.well_header[self.well_header['mnemonic'] == 'STEP'].reset_index(drop=True)['value'].iloc[0] + + dt_interval = np.nan_to_num(self.df[track].values) * increment / 1e6 + t_cum = np.cumsum(dt_interval) * 2 + self.df['TWT'] = t_cum + log_start_time + + self.add_curve_to_curve(original_mnemonic='TWT', + mnemonic='TWT', + descr='TWT', + unit='s') + + def calculate_acoustic_impedance(self, + velocity_track: Union[str, np.ndarray], + density_track: Union[str, np.ndarray], + add_to_well_logs: bool = True): + + if isinstance(velocity_track, str) and isinstance(density_track, str): + velocity = self.df[velocity_track].values + density = self.df[density_track].values + + values = np.c_[velocity, density] + + if isinstance(velocity_track, np.ndarray) and isinstance(density_track, np.ndarray): + values = np.c_[velocity_track, density_track] + + acoustic_impedance = np.apply_along_axis(np.product, -1, + values) + + if add_to_well_logs: + self.df['Acoustic Impedance'] = acoustic_impedance + + self.add_curve_to_curve(original_mnemonic='Acoustic Impedance', + mnemonic='Acoustic Impedance', + descr='Acoustic Impedance', + unit=' ') + else: + return acoustic_impedance + + def calculate_reflection_coefficient(self, + velocity_track: Union[str, np.ndarray], + density_track: Union[str, np.ndarray], + add_to_well_logs: bool = True): + + if add_to_well_logs: + self.calculate_acoustic_impedance(velocity_track=velocity_track, + density_track=density_track, + add_to_well_logs=add_to_well_logs) + acoustic_impedance = self.df['Acoustic Impedance'].values + + else: + acoustic_impedance = self.calculate_acoustic_impedance(velocity_track=velocity_track, + density_track=density_track, + add_to_well_logs=add_to_well_logs) + + reflection_coefficient = (acoustic_impedance[1:] - acoustic_impedance[:-1]) / ( + acoustic_impedance[1:] + acoustic_impedance[:-1]) + + reflection_coefficient = np.append(reflection_coefficient, 0) + + if add_to_well_logs: + + self.df['Reflection Coefficient'] = reflection_coefficient + + self.add_curve_to_curve(original_mnemonic='Reflection Coefficient', + mnemonic='Reflection Coefficient', + descr='Reflection Coefficient', + unit=' ') + + else: + return reflection_coefficient + + def resample_log_from_depth_to_time_domain(self, + dt: float, + track: str): + + if 'TWT' not in self.df.columns: + raise ValueError( + 'Please calculate time-depth relationship first before resampling log from depth to the time domain') + + t_max = self.df['TWT'].iloc[-1] + t = np.arange(0, t_max, dt) + + log_time_domain = np.interp(x=t, + xp=self.df['TWT'], + fp=self.df[track]) + + return log_time_domain + + def calculate_ricker_wavelet(self, + f, + length, + dt): + t0 = np.arange(-length / 2, (length - dt) / 2, dt) + y = (1.0 - 2.0 * (np.pi ** 2) * (f ** 2) * (t0 ** 2)) * np.exp(-(np.pi ** 2) * (f ** 2) * (t0 ** 2)) + return t0, y + + def calculate_synthetic_seismic(self, + t0, + w, + rc_tdom): + synth = np.apply_along_axis(lambda t0: np.convolve(t0, w, mode="same"), axis=0, arr=rc_tdom) + + return synth \ No newline at end of file diff --git a/pyborehole/logs_lis.py b/pyborehole/logs_lis.py new file mode 100644 index 0000000..8c2d76c --- /dev/null +++ b/pyborehole/logs_lis.py @@ -0,0 +1,232 @@ +from typing import Union, Tuple +import pandas as pd +import numpy as np +import matplotlib + + +class LISLogs: + """Class to initiate a Well Log Object. + + Parameters + __________ + path : str + Path to the well logs, e.g. ``path='logs.lis'``. + nodata : Union[int, float], default: ``-9999`` + No data value, e.g. ``nodata=-9999``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + Returns + _______ + LISLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.lis') + >>> borehole.logs.df + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + See Also + ________ + pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. + pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. + pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. + pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. + + .. versionadded:: 0.0.1 + """ + + # Initiate class + def __init__(self, + borehole, + path: str, + nodata: Union[int, float] = -9999): + """Class to initiate a Well Log Object. + + Parameters + __________ + path : str + Path to the well logs, e.g. ``path='logs.lis'``. + nodata : Union[int, float], default: ``-9999`` + No data value, e.g. ``nodata=-9999``. + + Attributes + __________ + df : pd.DataFrame + Pandas DataFrame containing the curves. Data columns are as follows: + + ======= ===================================== + Index Index of each measurment + DEPTH Measured depth of each measurment + Curves Columns containing the measurements + ======= ===================================== + + **Example:** + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + Returns + _______ + LISLogs + Well Log Object. + + Raises + ______ + TypeError + If the wrong input data types are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.lis') + >>> borehole.logs.df + + ======= ======= =========== ========= ========= ========== + Index DEPTH SGR.SG TH.SG U.SG K.SG + ======= ======= =========== ========= ========= ========== + 0 0.02 NaN NaN NaN NaN + 1 0.07 9.997668 1.991596 0.409476 0.247140 + 2 0.12 10.925511 2.279886 0.427854 0.262401 + 3 0.17 11.815056 2.566926 0.447979 0.273815 + 4 0.22 12.674678 2.846440 0.467957 0.284187 + ======= ======= =========== ========= ========= ========== + + See Also + ________ + pyborehole.borehole.Borehole.add_deviation : Add deviation to the Borehole Object. + pyborehole.borehole.Borehole.add_litholog : Add LithoLog to the Borehole Object. + pyborehole.borehole.Borehole.add_well_design : Add Well Design to the Borehole Object. + pyborehole.borehole.Borehole.add_well_tops : Add Well Tops to the Borehole Object. + + .. versionadded:: 0.0.1 + """ + # Importing dlisio + try: + from dlisio import lis + except ModuleNotFoundError: + ModuleNotFoundError('dlisio package not installed') + + # Checking that the path is provided as string + if not isinstance(path, str): + raise TypeError('The path must be provided as string') + + # Checking that the nodata is provided as float or int + if not isinstance(nodata, (float, int)): + raise TypeError('The nodata value must be provided as float or int') + + # Opening LIS file + f, *tail = lis.load(path) + + # Extracting curves + formatspecs = f.data_format_specs() + format_spec = formatspecs[0] + curves = lis.curves(f, format_spec) + + # Extracting column names + columns = [spec.mnemonic for spec in format_spec.specs] + + # Extracting units + units = [spec.units for spec in format_spec.specs] + + # Creating DataFrame from curves + df = pd.DataFrame(curves) + + # Assigning column names + df.columns = columns + + # Replace NaN Values + df = df.replace(nodata, np.NaN) + + # Extracting DataFrame from DLIS file + self.df = df + + # Changing the name of the depth column + self.df['DEPTH'] = self.df['DEPT'] + self.df = self.df.drop('DEPT', axis=1) + + # Creating Curves DataFrame + self.curves = pd.DataFrame(list(zip(columns, columns, columns, units)), + columns=['original_mnemonic', + 'mnemonic', + 'descr', + 'unit']) + + # Getting index of depth column + depth_column_index = columns.index('DEPT') + + # Defining mnemonics + mnemonic = ['STRT', 'STOP', 'STEP', 'NULL'] + + # Defining units + units = [format_spec.index_units.strip(), + format_spec.index_units.strip(), + format_spec.index_units.strip(), + ''] + + # Defining Values + value = [curves[depth_column_index][0], + curves[-1][0], + format_spec.spacing, + nodata] + + # Defining Description + descr = ['', '', '', ''] + + # Creating Well Header DataFrame + self.well_header = pd.DataFrame(list(zip(mnemonic, units, value, descr)), + columns=['mnemonic', + 'unit', + 'value', + 'descr']) + diff --git a/pyborehole/plot.py b/pyborehole/plot.py new file mode 100644 index 0000000..476680a --- /dev/null +++ b/pyborehole/plot.py @@ -0,0 +1,611 @@ +from typing import Union, Tuple +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + + +def plot_well_logs(logs, + tracks: Union[str, list] = None, + depth_column: str = 'DEPTH', + depth_min: Union[int, float] = None, + depth_max: Union[int, float] = None, + colors: Union[str, list] = None, + add_well_tops: bool = False, + add_well_design: bool = False, + fill_between: int = None, + add_net_to_gross: int = None, + fontsize_well_tops: Union[int, float] = 12, + use_df_tdom: bool = False) -> Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: + """Plot well logs. + + Parameters + __________ + + tracks : Union[str, list] + Name/s of the logs to be plotted, e.g. ``tracks='SGR'`` or ``tracks=['SGR', 'K']``. + depth_column : str, default: ``'DEPTH'`` + Name of the column holding the depths, e.g. ``depth_column='DEPTH'``. + depth_min : Union[int, float] + Minimum depth to be plotted, e.g. ``depth_min=0``. + depth_max : Union[int, float] + Maximum depth to be plotted, e.g. ``depth_max=2000``. + colors : Union[str, list], default: ``None`` + Colors of the logs, e.g. ``colors='black'`` or ``colors=['black', 'blue']``. + add_well_tops : bool, default: ``False`` + Boolean to add well tops to the plot, e.g. ``add_well_tops=True``. + add_well_design : bool, default: ``False`` + Boolean to add well design to the plot, e.g. ``add_well_design=True``. + fill_between : int, default: ``None`` + Number of the axis to fill, e.g. ``fill_between=0``. + add_net_to_gross : int, default: ``None`` + Number of axis to fill with the net to gross values, e.g. ``add_net_to_gross=0``. + + Returns + _______ + fig : matplotlib.figure.Figure + Matplotlib Figure. + ax : matplotlib.axes.Axes + Matplotlib Axes. + + Raises + ______ + TypeError + If the wrong input data types are provided. + ValueError + If no well tops are provided but ``add_well_tops`` is set to ``True``. + ValueError + If the wrong column names are provided. + + Examples + ________ + >>> import pyborehole + >>> from pyborehole.borehole import Borehole + >>> borehole = Borehole(name='Weisweiler R1') + >>> borehole.init_properties(location=(6.313031, 50.835676), crs='EPSG:4326', altitude_above_sea_level=136) + >>> borehole.add_well_logs(path='Well_logs.las') + >>> borehole.logs.plot_well_logs(tracks=['SGR', 'CAL'], depth_column='DEPTH', colors=['black', 'red']) + + See Also + ________ + plot_well_log_along_path : Plot well log along path. + calculate_vshale : Calculate Shale Volume. + calculate_vshale_linear : Calculate Shale Volume linear method. + calculate_net_to_gross : Calculate net to gross. + + .. versionadded:: 0.0.1 + """ + + # Selecting DataFrame + # Checking that the use_df_tdom boolean value is of + if not isinstance(use_df_tdom, bool): + raise TypeError('The use_df_tdom argument must be provided as bool') + + if use_df_tdom: + df = logs.df_tdom.copy(deep=True) + else: + df = logs.df.copy(deep=True) + + # Selecting Tracks + # Checking that the tracks are provided as list or string + if not isinstance(tracks, (list, str, type(None))): + raise TypeError('The track/s must bei either provided as str or a list of strings') + + # Checking that the depth column is of type string + if not isinstance(depth_column, str): + raise TypeError('Depth_column must be provided as string') + + # Checking that the depth column is in the DataFrame + if not {depth_column}.issubset(df): + raise ValueError('The depth_column named %s is not part of the curves' % depth_column) + + # Selecting tracks if one track is provided + if isinstance(tracks, str): + + # Checking that the track is a column of the DataFrame + if not {tracks}.issubset(df.columns): + raise ValueError('The track named %s is not part of the curves' % tracks) + + # Adding N/G if add_net_to_gross is provided + if add_net_to_gross: + if 'N/G' not in df.columns: + raise ValueError('Net to gross has not been calculated yet') + + net_to_gross = df['N/G'] + + # Selecting tracks + df = df[[tracks] + [depth_column]].reset_index() + + # Selecting tracks if no tracks were provided + elif tracks is None: + + # Getting tracks from columns of DataFrame + tracks = list(df.columns) + + # Removing depth or time column + if use_df_tdom: + tracks.remove('TWT') + else: + tracks.remove(depth_column) + + # Selecting tracks if tracks are provided as a list of string + elif isinstance(tracks, list): + + # Checking that the tracks are a column of the DataFrame + if not all({track}.issubset(df.columns) for track in tracks): + raise ValueError('Not all tracks are part of the curves') + + # Adding N/G if add_net_to_gross is provided + if add_net_to_gross: + if 'N/G' not in df.columns: + raise ValueError('Net to gross has not been calculated yet') + + net_to_gross = df['N/G'] + + # Selecting tracks + df = df[tracks + [depth_column]].reset_index() + + # Selecting Depth Range + # Checking that depth_min is of type float or int + if not isinstance(depth_min, (int, float, type(None))): + raise ValueError('The minimum depth must be provided as int or float') + + # Checking that depth_max is of type float or int + if not isinstance(depth_max, (int, float, type(None))): + raise ValueError('The maximum depth must be provided as int or float') + + # Selecting depth range + if not depth_min: + depth_min = min(df[depth_column]) + if not depth_max: + depth_max = max(df[depth_column]) + + # Setting buffer for plotting + buffer = (depth_max - depth_min) / 20 + + # Selecting depth range + df = df[(df[depth_column] >= depth_min) & (df[depth_column] <= depth_max)] + + # Checking that the colors are provided as list or string + if not isinstance(colors, (list, str, type(None))): + raise TypeError('The track/s must bei either provided as str or a list of strings') + + # Checking that the add_well_tops variable is of type bool + if not isinstance(add_well_tops, bool): + raise TypeError('The add_well_tops variable must be provided as bool') + + # Checking that the add_well_design variable is of type bool + if not isinstance(add_well_design, bool): + raise TypeError('The add_well_design variable must be provided as bool') + + # Checking that the fill_between variable is of type int + if not isinstance(fill_between, (int, type(None))): + raise TypeError('The fill_between variable must be provided as int') + + # Checking that the add_net to_gross variable is of type bool + if not isinstance(add_net_to_gross, (int, type(None))): + raise TypeError('The add_net_to_gross variable must be provided as int') + + # Helping variable for adding well tops + if add_well_tops: + j = 1 + else: + j = 0 + + # Helping variable for adding well design + if add_well_design: + k = 1 + else: + k = 0 + + # Creating plot if tracks is of type string + if isinstance(tracks, str): + + # Helping variable for adding net to gross + if add_net_to_gross: + l = 1 + else: + l = 0 + + # Creating plot + fig, ax = plt.subplots(1, + 1 + j + k + l, + figsize=(1 + j + k + l * 1.8, 8), + sharey=True) + + if not add_well_tops and not add_net_to_gross: + ax = [ax, None] + + # Plotting track + ax[j + k].plot(df[tracks], + df[depth_column], + color=colors) + + # Setting Grid + ax[j + k].grid() + + # Inverting y axis + ax[j + k].invert_yaxis() + + # Setting y limits + ax[j + k].set_ylim(depth_max + buffer, + depth_min - buffer) + + # Setting x limits + margin = (max(df[tracks].dropna()) - min(df[tracks].dropna())) * plt.margins()[1] + ax[j + k].set_xlim(min(df[tracks].dropna()) - margin, max(df[tracks].dropna()) + margin) + + # Setting tick params + ax[j + k].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + + # Setting x label position + ax[j + k].xaxis.set_label_position('top') + + # Setting x label + ax[j + k].set_xlabel(tracks + ' [%s]' % + logs.curves[logs.curves['original_mnemonic'] == tracks].reset_index(drop=True)[ + 'unit'].iloc[ + 0], color='black') + + # Setting y label + ax[0 + k].set_ylabel(depth_column + ' [%s]' % logs.curves.at[0, 'unit']) + + # Fill between curve + if fill_between == 0: + + # Obtaining left value + left_col_value = np.min(df[tracks].dropna()) + + # Obtaining right value + right_col_value = np.max(df[tracks].dropna()) + + # Calculating span + span = abs(left_col_value - right_col_value) + + # Getting color map + cmap = plt.get_cmap('hot_r') + + # Obtaining color index + color_index = np.arange(left_col_value, right_col_value, span / 100) + + # Looping through each value in the color_index + for index in sorted(color_index): + # Getting index value + index_value = (index - left_col_value) / span + + # Obtaining color for color index value + color = cmap(index_value) + + # Filling curve + ax[j + k].fill_betweenx(df[depth_column], + df[tracks], + left_col_value, + where=df[tracks] >= index, + color=color) + + # Adding well tops + if add_well_tops: + + # Checking that the logs object has well tops + if not logs.has_well_tops: + raise ValueError('Please add well tops to the borehole object to plot them') + else: + # Cropping well tops DataFrame to selected depth interval + df_well_tops_selected = logs.well_tops.df[ + (logs.well_tops.df['MD'] > depth_min) & (logs.well_tops.df['MD'] < depth_max)] + + # Plotting well tops + for index, row in df_well_tops_selected.iterrows(): + # Plotting lines + ax[0 + k].axhline(row[logs.well_tops.df.columns[1]], 0, 1, color='black') + + # Plotting text + ax[0 + k].text(0.05, row[df_well_tops_selected.columns[1]] - depth_max / 150, + s=row[logs.well_tops.df.columns[0]], + fontsize=fontsize_well_tops) + + # Setting grid + ax[0 + k].grid() + + # Setting x label position + ax[0 + k].xaxis.set_label_position('top') + + # Setting x label + ax[0 + k].set_xlabel('Well Bases') + + # Remove x ticks + ax[0 + k].axes.get_xaxis().set_ticks([]) + + # Adding net to gross + if add_net_to_gross is not None: + # Removing duplicated columns to avoid error + df_selected = df.loc[:, ~df.columns.duplicated()].copy() + + # Readding net to gross to DataFrame + df_selected['N/G'] = net_to_gross[df_selected.index].values + + ax[j + k + l].fill_betweenx(df_selected[depth_column], + df_selected[tracks], + 0, + where=df_selected['N/G'] == 1, + color='yellow') + ax[j + k + l].fill_betweenx(df_selected[depth_column], + df_selected[tracks], + 0, + where=df_selected['N/G'] == 0, + color='brown') + + # Setting Grid + ax[j + k + l].grid() + + # Plotting track + ax[j + k + l].plot(df[tracks], + df[depth_column], + color=colors) + + # Setting tick params + ax[j + k + l].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + + # Setting x label position + ax[j + k + l].xaxis.set_label_position('top') + + # Setting x label + ax[j + k + l].set_xlabel(tracks + ' [%s]' % + logs.curves[logs.curves['original_mnemonic'] == tracks].reset_index(drop=True)[ + 'unit'].iloc[ + 0], color='black') + + return fig, ax + + # Creating plot if tracks is of type list + elif isinstance(tracks, list): + + # Creating plot + fig, ax = plt.subplots(1, + len(tracks) + j + k, + figsize=((len(tracks) + j + k) * 1.8, 8), + sharey=True) + + # Setting colors + if not colors: + colors = [None] * len(tracks) + + # Setting y limits + ax[0].set_ylim(depth_max + buffer, + depth_min - buffer) + + # Setting y label + ax[0].set_ylabel(depth_column + ' [%s]' % logs.curves.at[0, 'unit']) + + # Plotting tracks + for i in range(len(tracks)): + # Plotting tracks + ax[i + j + k].plot(df[tracks[i]], df[depth_column], color=colors[i]) + + # Setting grid + ax[i + j + k].grid() + + # Inverting y axis + ax[i + j + k].invert_yaxis() + + # Setting y limits + ax[i + j + k].set_ylim(depth_max + buffer, + depth_min - buffer) + + # Removing duplicated columns to avoid error + df_selected = df.loc[:, ~df.columns.duplicated()].copy() + + # Setting x limits + margin = (max(df_selected[tracks[i]].dropna()) - min(df_selected[tracks[i]].dropna())) * plt.margins()[1] + ax[i + j + k].set_xlim(min(df_selected[tracks[i]].dropna()) - margin, + max(df_selected[tracks[i]].dropna()) + margin) + + # Setting tick params + ax[i + j + k].tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + + # Setting x label position + ax[i + j + k].xaxis.set_label_position('top') + + # Setting x label + ax[i + j + k].set_xlabel(tracks[i] + ' [%s]' % + logs.curves[logs.curves['original_mnemonic'] == tracks[i]].reset_index( + drop=True)[ + 'unit'].iloc[0], + color='black' if isinstance(colors[i], type(None)) else colors[i]) + + # Fill between curves + if fill_between is not None: + + # Obtaining left value + left_col_value = np.min(df[tracks[fill_between]].dropna()) + + # Obtaining right value + right_col_value = np.max(df[tracks[fill_between]].dropna()) + + # Calculating span + span = abs(left_col_value - right_col_value) + + # Getting color map + cmap = plt.get_cmap('hot_r') + + # Obtaining color index + color_index = np.arange(left_col_value, right_col_value, span / 100) + + # Dropping duplicate columns if the same track is selected twice + try: + # Removing duplicated columns to avoid error + df_selected = df.loc[:, ~df.columns.duplicated()][tracks[fill_between]].copy() + except AttributeError: + # Selecting Data + df_selected = df[tracks[fill_between]].copy(deep=True) + + # Looping through each value in the color_index + for index in sorted(color_index): + # Getting index value + index_value = (index - left_col_value) / span + + # Obtaining color for color index value + color = cmap(index_value) + + # Filling curve + ax[fill_between + j].fill_betweenx(df[depth_column], + df_selected.values.flatten(), + left_col_value, + where=df_selected.values.flatten() >= index, + color=color) + + # Adding well tops + if add_well_tops: + + # Checking that the logs object has well tops + if not logs.has_well_tops: + raise ValueError('Please add well tops to the borehole object to plot them') + else: + # Cropping well tops DataFrame to selected depth interval + df_well_tops_selected = logs.well_tops.df[ + (logs.well_tops.df['MD'] > depth_min) & (logs.well_tops.df['MD'] < depth_max)] + + # Plotting well tops + for index, row in df_well_tops_selected.iterrows(): + # Plotting lines + ax[0 + k].axhline(row[logs.well_tops.df.columns[1]], 0, 1, color='black') + + # Plotting text + ax[0 + k].text(0.05, row[df_well_tops_selected.columns[1]] - depth_max / 150, + s=row[logs.well_tops.df.columns[0]], + fontsize=fontsize_well_tops) + + # Setting grid + ax[0 + k].grid() + + # Setting x label position + ax[0 + k].xaxis.set_label_position('top') + + # Setting x label + ax[0 + k].set_xlabel('Well Bases') + + # Remove x ticks + ax[0 + k].axes.get_xaxis().set_ticks([]) + + # Adding net to gross + if add_net_to_gross is not None: + # Removing duplicated columns to avoid error + df_selected = df.loc[:, ~df.columns.duplicated()].copy() + + # Readding net to gross to DataFrame + df_selected['N/G'] = net_to_gross[df_selected.index].values + + ax[add_net_to_gross + j].fill_betweenx(df_selected[depth_column], + df_selected[tracks[add_net_to_gross]], + 0, + where=df_selected['N/G'] == 1, + color='yellow') + ax[add_net_to_gross + j].fill_betweenx(df_selected[depth_column], + df_selected[tracks[add_net_to_gross]], + 0, + where=df_selected['N/G'] == 0, + color='brown') + + plt.tight_layout() + + # Adding well design + if add_well_design: + if not logs.has_well_design: + raise ValueError('Please add a well design to the borehole object to plot it') + else: + for key, elem in logs.well_design.pipes.items(): + if elem.pipe_type == 'open hole section': + # Plotting open hole section as wavy line + y = np.linspace(elem.top, elem.bottom, 1000) + x1 = 0.5 * np.sin(y / 2) - elem.inner_diameter + 0.5 + x2 = 0.5 * np.cos(y / 2 + np.pi / 4) + elem.inner_diameter + ax[k].plot(x1, y, color='black') + ax[k].plot(x2, y, color='black') + # Plotting open hole section as dashed line + # ax.plot([elem.inner_diameter,elem.inner_diameter], + # [elem.top, elem.bottom], + # linestyle='--', + # color='black') + # ax.plot([-elem.inner_diameter, -elem.inner_diameter], + # [elem.top, elem.bottom], + # linestyle='--', + # color='black') + else: + ax[k].add_patch(Rectangle(elem.xy, elem.width, elem.height, color="black")) + ax[k].add_patch( + Rectangle((-1 * elem.xy[0], elem.xy[1]), -1 * elem.width, elem.height, color="black")) + + # Deep copy of pipes dict + x = copy.deepcopy(logs.pipes) + + # Popping open hole section + try: + type_list = [elem.pipe_type for key, elem in x.items()] + index_open_hole = type_list.index('open hole section') + key_open_hole = list(x.keys())[index_open_hole] + x.pop(key_open_hole) + except ValueError: + pass + + # Getting diameters and calculating thickness of cement between each pipe + outer_diameters = sorted([elem.outer_diameter for key, elem in x.items()], reverse=False) + inner_diameters = sorted([elem.inner_diameter for key, elem in x.items()], reverse=False) + thicknesses = [y - x for x, y in zip(outer_diameters[:-1], inner_diameters[1:])] + + # Sorting pipes + pipes_sorted = {k: v for k, v in sorted(x.items(), key=lambda item: item[1].outer_diameter)} + + # Selecting pies + pipes_selected = {k: pipes_sorted[k] for k in list(pipes_sorted)[:len(thicknesses)]} + + # Plotting top of pipe + i = 0 + for key, elem in pipes_selected.items(): + i = i + ax[k].add_patch( + Rectangle((elem.inner_diameter, elem.top), thicknesses[i] + elem.thickness, 1, + color="black")) + ax[k].add_patch(Rectangle((-1 * elem.inner_diameter, elem.top), + -1 * thicknesses[0] - elem.thickness, 1, + color="black")) + i = i + 1 + + # Plotting Casing Shoes + for key, elem in logs.well_design.pipes.items(): + if elem.shoe_width is not None: + p0 = [elem.outer_diameter, elem.bottom] + p1 = [elem.outer_diameter, elem.bottom - elem.shoe_height] + p2 = [elem.outer_diameter + elem.shoe_width, elem.bottom] + shoe = plt.Polygon([p0, p1, p2], color="black") + ax.add_patch(shoe) + p0[0] *= -1 + p1[0] *= -1 + p2 = [-elem.outer_diameter - elem.shoe_width, elem.bottom] + shoe = plt.Polygon([p0, p1, p2], color="black") + ax[k].add_patch(shoe) + + # Adding Casing Cements + for key, elem in logs.well_design.cements.items(): + ax[k].fill_between(elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) + ax[k].fill_between(-1 * elem.xvals, elem.tops, elem.bottoms, color="#6b705c", alpha=0.5) + + # Calculating axes limits + top = np.min([elem.top for key, elem in logs.well_design.pipes.items()]) + bottom = np.max([elem.bottom for key, elem in logs.well_design.pipes.items()]) + max_diam = np.max([elem.outer_diameter for key, elem in logs.well_design.pipes.items()]) + + # Setting axes limits + if not depth_min: + depth_min = min(df[depth_column]) + if not depth_max: + depth_max = max(df[depth_column]) + + buffer = (depth_max - depth_min) / 20 + + ax[k].set_ylim(depth_max + buffer, depth_min - buffer) + ax.set_xlim(-max_diam * 1, max_diam * 1) + # ax.invert_yaxis() + + # Setting axes labels + ax.set_xlabel('Diameter [in]') + + return fig, ax