Source code for edafos.soil.profile

""" Provide the ``SoilProfile`` class.

"""

# -- Imports -----------------------------------------------------------------
from edafos.project import Project
from tabulate import tabulate
import numpy as np
import pandas as pd


# -- SoilProfile Class -------------------------------------------------------

[docs]class SoilProfile(Project): """ Class to represent a new soil profile. .. warning:: Pay attention to the base units for each unit system that you choose to use. Refer to the parameter definition below or the :ref:`input_units` page. """ # -- Constructor ---------------------------------------------------------
[docs] def __init__(self, unit_system, water_table): """ Args: water_table (float): Depth to water table measured from ground elevation. - For **SI**: Enter value in **meters**. - For **English**: Enter value in **feet**. unit_system (str): The unit system for the project. Can only be 'English', or 'SI'. Properties inherited from the ``Project`` class. """ super().__init__(unit_system=unit_system) # Set units for the water table self.water_table = float(water_table) * self.set_units('length') # Call function to instantiate the soil profile data frame self._create_profile()
# -- Soil Profile Instantiation Method (Private) -------------------------
[docs] def _create_profile(self): """ A private method that instantiates the soil profile data frame. Returns: An empty Pandas DataFrame with two headers, one for the column names and another for the column units. """ # Careful when changing column named. Update API.get_soil_prop name_list = ['Soil Type', 'Soil Desc', 'Depth', 'Height', 'TUW', 'Field N', 'Corr. N', 'Field Phi', 'Calc. Phi', 'Shear Su'] self.layers = pd.DataFrame(columns=name_list) self.layers.index.name = 'Layer' return self
# -- Method to add layers ------------------------------------------------
[docs] def add_layer(self, soil_type, height, **kwargs): """ Method to add a new layer to the soil profile. .. todo:: Run parameter checks for allowable ranges and required info, for example, raise a warning for a cohesionless layer without shear strength. Args: soil_type (str): Allowed values are 'cohesive' for clays and 'cohesionless' for sands. height (float): Height of soil layer. - For **SI**: Enter height in **meters**. - For **English**: Enter height in **feet**. Keyword Args: soil_desc (str): Soil description. Initially created to accommodate the :ref:`olson90-method`. As such, in order to follow the guidelines in :numref:`Olson90_table`, the only valid inputs are: ``gravel``, ``sand-gravel``, ``sand``, ``sand-silt``, ``silt``. TODO: There is no check to reject these inputs for ``soil_type = 'cohesive'``, although they have no effect. tuw (float): Total unit weight of soil. - For **SI**: Enter TUW in **kN/m**\ :sup:`3`. - For **English**: Enter TUW in **lbf/ft**\ :sup:`3`. field_n (int): Field SPT-N values. corr_n (int): Corrected Field SPT-N values. .. note:: If field SPT-N value is given without the corrected SPT-N, the corrected value will be automatically calculated. field_phi (float): Field internal angle of friction, *φ*, in degrees. calc_phi (float): Calculated internal angle of friction, *φ*, from SPT-N values. su (float): Undrained shear strength, *s*\ :sub:`u`. - For **SI**: Enter *s*\ :sub:`u` in **kN/m**\ :sup:`2`. - For **English**: Enter *s*\ :sub:`u` in **kip/ft**\ :sup:`2`. """ # Check for valid attributes # If you update these keys, make sure to update API.get_soil_prop too allowed_keys = ['soil_type', 'soil_desc', 'height', 'tuw', 'field_n', 'corr_n', 'field_phi', 'calc_phi', 'su'] for key in kwargs: if key not in allowed_keys: raise AttributeError("'{}' is not a valid attribute. The " "allowed attributes are: {}" "".format(key, allowed_keys)) # Assign values soil_desc = kwargs.get('soil_desc', None) tuw = kwargs.get('tuw', None) field_n = kwargs.get('field_n', None) corr_n = kwargs.get('corr_n', None) field_phi = kwargs.get('field_phi', None) calc_phi = kwargs.get('calc_phi', None) su = kwargs.get('su', None) # Check for soil type if soil_type in ['cohesive', 'cohesionless']: soil_type = soil_type else: raise ValueError("Soil type can only be 'cohesive' or " "'cohesionless'.") # Check for soil description allowed_soil_desc = ['gravel', 'sand-gravel', 'sand', 'sand-silt', 'silt'] if (soil_desc is not None) and (soil_desc not in allowed_soil_desc): raise ValueError("'{}' is not a valid soil description input.\n" "Valid inputs are: {}." "".format(soil_desc, allowed_soil_desc)) # Check that all inputs are positive numbers for i in [height, tuw, field_n, corr_n, field_phi, calc_phi, su]: if (i is not None) and (type(i) not in [int, float]): raise TypeError("Value '{}' is of type {} and is not " "permissible. \nEnter only positive numbers " "(int or float) for soil properties." "".format(i, type(i))) elif (i is not None) and (i < 0): raise ValueError("Value '{}' is not permissible. Enter positive" " numbers only for soil properties.".format(i)) else: pass # Calculate depth from layers heights if len(self.layers) == 0: depth = height else: depth = self.layers.loc[len(self.layers), 'Depth'] + height # Store values in data frame self.layers.loc[len(self.layers)+1] = [ soil_type, soil_desc, depth, height, tuw, field_n, corr_n, field_phi, calc_phi, su] # Reset index to start at 1 if self.layers.index[0] == 0: self.layers.index = self.layers.index + 1 # Enforce proper data types # TODO: Repeated every time a layer is added. Is there a better way? self.layers.fillna(np.nan, inplace=True) # for column in self.layers.columns.levels[0]: for column in self.layers.columns: if column not in ['Soil Type', 'Soil Desc']: self.layers[column] = self.layers[column].astype(float) return self
# -- Method that returns list of relevant z's ----------------------------
[docs] def z_of_layers(self, loc='bot'): """ Method that returns a list of depths, :math:`z`, for the defined soil profile layers. The depths selected are at the layer interface and at layer midpoint. The method returns a list with either or all points based on the input of the ``loc`` argument. This list can be used for effective stress calculations. Args: loc (str): Controls the selection of points. Available options are ``bot`` (for bottom of layer), ``mid`` (for layer midpoint) or ``all``. Returns: list: A list of depths, :math:`z` (unitless). """ # Check that the input is valid allowed = ['bot', 'mid', 'all'] if loc not in allowed: raise ValueError("'{}' is not a valid input. Available options are " "{}.".format(loc, allowed)) else: pass bot_list = self.layers['Depth'].values mid_list = bot_list - self.layers['Height'].values / 2 if loc == 'bot': z_list = [0] + bot_list.tolist() elif loc == 'mid': z_list = [0] + mid_list.tolist() else: z_list = [0] + bot_list.tolist() + mid_list.tolist() z_list.sort() return z_list
# -- Method to calculate stresses ----------------------------------------
[docs] def calculate_stress(self, z, kind='effective'): """ Method to calculate stresses (pore water, total, effective). It defaults to 'effective'. Change the ``kind`` parameter to get the other stresses. Args: z (float): Vertical depth to the point of interest, measured from the top of the soil profile. - For **SI**: Enter depth, *z*, in **meters**. - For **English**: Enter depth, *z*, in **feet**. kind (str): Parameter that controls the output of the function. Allowed values are ``total``, ``pore_water``, ``effective`` and ``all``. The last value, ``all``, returns all three stresses in the same order. Returns: Quantity: A physical quantity with associated units. - For **SI**: Stress is returned in, **kN/m**\ :sup:`2`. - For **English**: Stress is returned in, **kip/ft**\ :sup:`2`. """ # Check for kind values allowed = ['effective', 'total', 'pore_water', 'all'] if kind in allowed: kind = kind else: raise ValueError("'{}' entry is invalid. Choose from {}." "".format(kind, allowed)) # Check that z is within limits max_depth = self.layers['Height'].sum() if z > max_depth: raise ValueError("Depth z = {0} {2}, is beyond the total defined " "soil profile depth, {1} {2}." "".format(z, max_depth, self.set_units('length'))) elif ((z < 0) and (self.water_table.magnitude >= 0)) \ or (z < self.water_table.magnitude < 0): raise ValueError("Nothing but thin air at z = {} {}. Try lower." "".format(z, self.set_units('length'))) else: pass # Set units for input parameter, z z = float(z) * self.set_units('length') # Define zw, the vertical distance below the water table. zw = z - self.water_table if zw.magnitude < 0: zw = 0 * zw.units # Define pore water pressure if self.unit_system == 'SI': gamma_w = 9.81 * self.set_units('tuw') else: gamma_w = 62.4 * self.set_units('tuw') pore_water = zw * gamma_w # -- Define total stress --------------------------------------------- h1 = self.layers['Height'][1] * self.set_units('length') g1 = self.layers['TUW'][1] * self.set_units('tuw') # Prepare for Takeaway No 4 if (z.magnitude >= 0) and (self.water_table.magnitude < 0): stress_from_water_body = abs(self.water_table) * gamma_w else: stress_from_water_body = 0 * self.set_units('stress') # Main if statement if (z.magnitude < 0) and (self.water_table.magnitude < 0): total_stress = pore_water elif z < h1: total_stress = z * g1 + stress_from_water_body elif z.magnitude in self.layers['Depth'].values: # Get the layer index where z is at the interface ix = self.layers[self.layers['Depth'] == z.magnitude].index[0] total_stress = sum((self.layers['Height'][0:ix].values * self.set_units('length')) * (self.layers['TUW'][0:ix].values * self.set_units('tuw')) ) + stress_from_water_body else: # Get the previous layer index where z is in ixp = self.layers[self.layers['Depth'] < z.magnitude].index[-1] # Get the current layer index where z is in ixc = self.layers[self.layers['Depth'] > z.magnitude].index[0] total_stress = (sum((self.layers['Height'][0:ixp].values * self.set_units('length')) * (self.layers['TUW'][0:ixp].values * self.set_units('tuw')) ) + ( ((z.magnitude - self.layers['Depth'][ixp]) * self.set_units('length')) * (self.layers['TUW'][ixc] * self.set_units('tuw'))) ) + stress_from_water_body # Define effective stress effective_stress = total_stress - pore_water if kind == 'effective': return effective_stress elif kind == 'total': return total_stress elif kind == 'pore_water': return pore_water else: return total_stress, pore_water, effective_stress
# -- Method that returns soil properties given z -------------------------
[docs] def get_soil_prop(self, z, sp): """ This method will return the soil property (with units), i.e. SPT-N, total unit weight, undrained shear strength, etc. at depth, :math:`z`. The soil properties must have been previously defined with the :meth:`~edafos.soil.profile.SoilProfile.add_layer` method. TODO: This method will be updated to locate more granular data prior to layer delineation, if available. Args: z (float): Vertical depth to the point of interest, measured from the top of the soil profile. - For **SI**: Enter depth, *z*, in **meters**. - For **English**: Enter depth, *z*, in **feet**. sp (str): Available inputs exactly as defined in the keyword arguments of :meth:`~edafos.soil.profile.SoilProfile.add_layer`. Returns: Quantity: Soil property with units. """ # Shorthand for convenience df = self.layers # z check if z < 0: raise ValueError("z cannot be negative.") elif z > df['Depth'].max(): raise ValueError("z cannot be larger than max soil profile depth.") # Input check allowed = ['soil_type', 'soil_desc', 'height', 'tuw', 'field_n', 'corr_n', 'field_phi', 'calc_phi', 'su'] if sp not in allowed: raise ValueError("'{}' is not a valid input. Allowed inputs are {}" ".".format(sp, allowed)) cond = df[df['Depth'] < z].index.max() index = 1 if cond is np.nan else cond + 1 if sp == 'soil_type': value = df['Soil Type'][index] elif sp == 'soil_desc': value = df['Soil Desc'][index] elif sp == 'height': value = df['Height'][index] * self.set_units('length') elif sp == 'tuw': value = df['TUW'][index] * self.set_units('tuw') elif sp == 'field_n': value = df['Field N'][index] elif sp == 'corr_n': value = df['Corr. N'][index] elif sp == 'field_phi': value = df['Field Phi'][index] * self.set_units('degrees') elif sp == 'calc_phi': value = df['Calc. Phi'][index] * self.set_units('degrees') else: # su value = df['Shear Su'][index] * self.set_units('stress') return value
# -- Method for string representation ------------------------------------ def __str__(self): layer_tbl = tabulate(self.layers, headers='keys', tablefmt='simple') unit_list = [ ['Depth', '(m)' if self.unit_system == 'SI' else '(ft)'], ['Height', '(m)' if self.unit_system == 'SI' else '(ft)'], ['TUW', '(kN/m3)' if self.unit_system == 'SI' else '(lbf/ft3)'], ['Field N', '(bl/0.3m)' if self.unit_system == 'SI' else '(bpf)'], ['Corrected N', '(bl/0.3m)' if self.unit_system == 'SI' else '(bpf)'], ['Field Phi', '(deg)'], ['Calculated Phi', '(deg)'], ['Shear Strength', '(kN/m2)' if self.unit_system == 'SI' else '(lbf/ft2)'] ] unit_tbl = tabulate(unit_list, tablefmt='plain') return "\n" \ "Unit System: {0.unit_system}\n" \ "Water Table: {0.water_table}\n\n" \ "{1}\n\n" \ "Units:\n" \ "------\n" \ "{2}".format(self, layer_tbl, unit_tbl)