├── docs ├── index.md ├── package.md ├── references.md ├── requirements.txt └── history.md ├── .github └── ISSUE_TEMPLATE.md ├── pysigmap ├── __init__.py ├── pachecosilva.py ├── boone.py ├── bilog.py ├── casagrande.py ├── data.py └── energy.py ├── .readthedocs.yaml ├── pyproject.toml ├── mkdocs.yml ├── LICENSE ├── .gitignore ├── illustrative_examples └── illustrative_examples.py └── README.md /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | {!README.md!} 4 | 5 | 6 | The following is a copy of the license: 7 | 8 | 9 | {!LICENSE!} 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * pySigmaP version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /pysigmap/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Application software for calculating the preconsolidation pressure from the incremental loading oedometer testing""" 4 | 5 | __all__ = ["data", "casagrande", "energy", "bilog", "pachecosilva", "boone"] 6 | __version__ = "0.1.10" 7 | 8 | # Global variables 9 | figsize = [10, 5.3] 10 | # High-contrast qualitative colour scheme 11 | colors = ("#DDAA33", "#BB5566", "#004488") # yellow # red # blue 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.9" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /docs/package.md: -------------------------------------------------------------------------------- 1 | # Package structure 2 | 3 | **`pySigmaP`** is written under the object-oriented paradigm and divided into six modules. One of them contains the class to load and manage the data of the compressibility curve, and the other five contain the classes to determine the preconsolidation pressure via nine different methods. 4 | 5 | Each module is documented and basic examples are illustrated. 6 | 7 | ::: pysigmap.data 8 | handler: python 9 | 10 | ::: pysigmap.casagrande 11 | handler: python 12 | 13 | ::: pysigmap.energy 14 | handler: python 15 | 16 | ::: pysigmap.bilog 17 | handler: python 18 | 19 | ::: pysigmap.pachecosilva 20 | handler: python 21 | 22 | ::: pysigmap.boone 23 | handler: python 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pysigmap" 7 | authors = [ 8 | {name = "Exneyder A. Montoya-Araque", email = "eamontoyaa@gmail.com"}, 9 | {name = "A. J. Aparicio-Ortube", email = "aaparicioo@unal.edu.co"}, 10 | {name = "D. G. Zapata-Medina", email = "dgzapata@unal.edu.co"}, 11 | {name = "L. G. Arboleda-Monsalve", email = "luis.arboleda@ucf.edu"}, 12 | ] 13 | readme = "README.rst" 14 | license = {file = "LICENSE"} 15 | classifiers = ["License :: OSI Approved :: MIT License"] 16 | dynamic = ["version", "description"] 17 | requires-python = ">=3.7" 18 | dependencies = [ 19 | "numpy >=1.23.5", 20 | "matplotlib >=3.7.1", 21 | "scipy >= 1.11.3", 22 | "pandas >= 1.5.3", 23 | "mstools >= 0.1.0" 24 | ] 25 | 26 | [project.urls] 27 | Source = "https://github.com/eamontoyaa/pysigmap" 28 | Documentation = "https://pysigmap.readthedocs.io/en/latest/" 29 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pySigmaP Docs 2 | repo_url: https://github.com/eamontoyaa/pySigmaP 3 | site_author: Exneyder A. Montoya-Araque, A. J. Aparicio-Ortube, D. G. Zapata-Medina, and L. G. Arboleda-Monsalve 4 | copyright: Copyright © 2023 Exneyder A. Montoya-Araque, A. J. Aparicio-Ortube, D. G. Zapata-Medina, and L. G. Arboleda-Monsalve 5 | 6 | nav: 7 | - Home: index.md 8 | - Illustrative example: tutorials/01_illustrative_example.ipynb 9 | - Package docstrings: package.md 10 | - History: history.md 11 | - References: references.md 12 | 13 | theme: 14 | name: readthedocs 15 | highlightjs: true 16 | 17 | plugins: 18 | - search 19 | - mkdocstrings: 20 | handlers: 21 | # See: https://mkdocstrings.github.io/python/usage/ 22 | python: 23 | options: 24 | docstring_style: numpy 25 | show_root_heading: true 26 | show_source: true 27 | show_signature: true 28 | separate_signature: true 29 | line_length: 80 30 | - mkdocs-jupyter: 31 | include: ["*.ipynb"] 32 | # ignore_h1_titles: True 33 | execute: false 34 | include_source: True 35 | 36 | markdown_extensions: 37 | - markdown_include.include: 38 | base_path: ./ 39 | # - admonition 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 4 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 5 | UNIVERSIDAD NACIONAL DE COLOMBIA. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /docs/references.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | Becker, D. E., Crooks, J. H. A., Been, K., & Jefferies, M. G. (1987). Work as a 4 | criterion for determining in situ and yield stresses in clays. Canadian 5 | Geotechnical Journal, 24, 4, 549-564, https://doi.org/10.1139/t87-070. 6 | 7 | Boone, S. J. (2010). A critical reappraisal of "preconsolidation 8 | pressure" interpretations using the oedometer test. Canadian Geotechnical 9 | Journal, 47, 3, 281-296. https://doi.org/10.1139/T09-093. 10 | 11 | Butterfield, R. (1979). A natural compression law for soils (an advance on 12 | e–log p'). Geotechnique, 29, 4, 469-480, 13 | https://doi.org/10.1680/geot.1979.29.4.469. 14 | 15 | Casagrande, A. (1936). The determination of pre-consolidation load and its 16 | practical significance. In Proceedings of the First International Conference 17 | on Soil Mechanins and Foundations Engineering, 3, 60-64. 18 | 19 | Morin, P. (1988). Work as a criterion for determining in situ and yield 20 | stresses in clays: Discussion. Canadian Geotechnical Journal, 25, 4, 845-847, 21 | https://doi.org/10.1139/t88-096. 22 | 23 | Oikawa, H. (1987). Compression Curve of Soft Soils. Soils and Foundations, 24 | 27, 3, 99-104, https://doi.org/10.3208/sandf1972.27.3_99. 25 | 26 | Onitsuka, K., Hong, Z., Hara, Y., & Yoshitake, S. (1995). Interpretation of 27 | Oedometer Test Data for Natural Clays. Soils and Foundations, 35, 3, 61-70, 28 | https://doi.org/10.3208/sandf.35.61. 29 | 30 | Pacheco Silva, F. 1970. A new graphical construction for determination of the 31 | pre-consolidation stress of a soil sample. In Proceedings of the 4th Brazilian 32 | Conference on Soil Mechanics and Foundation Engineering, Rio de Janeiro, 33 | Brazil. Vol. 2, No.1, pp. 225–232. 34 | 35 | Wang, L. B., & Frost, J. D. (2004). Dissipated strain energy method for 36 | determining preconsolidation pressure. Canadian Geotechnical Journal, 41, 4, 37 | 760-768, https://doi.org/10.1139/t04-013. 38 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.10 3 | # To update, run: 4 | # 5 | # pip-compile docs/requirements.in 6 | # 7 | # click==8.1.3 8 | # # via mkdocs 9 | # ghp-import==2.1.0 10 | # # via mkdocs 11 | # griffe==0.22.0 12 | # # via mkdocstrings-python 13 | # importlib-metadata==4.12.0 14 | # # via mkdocs 15 | # jinja2==3.1.2 16 | # # via 17 | # # mkdocs 18 | # # mkdocstrings 19 | # markdown==3.5 20 | # # via 21 | # # markdown-include 22 | # # mkdocs 23 | # # mkdocs-autorefs 24 | # # mkdocstrings 25 | # # pymdown-extensions 26 | # markdown-include==0.8.1 27 | # # via -r docs/requirements.in 28 | # markupsafe==2.1.1 29 | # # via 30 | # # jinja2 31 | # # mkdocstrings 32 | # mergedeep==1.3.4 33 | # # via mkdocs 34 | # mkdocs==1.5.3 35 | # # via 36 | # # -r docs/requirements.in 37 | # # mkdocs-autorefs 38 | # # mkdocstrings 39 | # mkdocs-autorefs==0.4.1 40 | # # via mkdocstrings 41 | # mkdocstrings[python]==0.23.0 42 | # # via 43 | # # -r docs/requirements.in 44 | # # mkdocstrings-python 45 | # mkdocstrings-python==1.7.3 46 | # # via mkdocstrings 47 | # packaging==21.3 48 | # # via mkdocs 49 | # pymdown-extensions==9.5 50 | # # via mkdocstrings 51 | # pyparsing==3.0.9 52 | # # via packaging 53 | # python-dateutil==2.8.2 54 | # # via ghp-import 55 | # pyyaml==6.0 56 | # # via 57 | # # mkdocs 58 | # # pyyaml-env-tag 59 | # pyyaml-env-tag==0.1 60 | # # via mkdocs 61 | # six==1.16.0 62 | # # via python-dateutil 63 | # watchdog==2.1.9 64 | # # via mkdocs 65 | # zipp==3.8.0 66 | # # via importlib-metadata 67 | 68 | 69 | # mkdocs-material==9.4.7 70 | # mkdocs-material-extensions==1.3 71 | # mkdocs-jupyter==0.24.6 72 | 73 | markdown==3.5 74 | mkdocs==1.5.3 75 | mkdocstrings==0.23.0 76 | mkdocstrings[python]>=0.18 77 | markdown-include==0.8.1 78 | mkdocs-jupyter==0.24.6 79 | mkdocs-material==9.4.7 80 | mkdocstrings==0.23.0 81 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 0.1.0 (2020-10-10) 4 | 5 | * First release on PyPI. 6 | 7 | ## 0.1.1 (2020-10-10) 8 | 9 | * Minor updates to html documentation. 10 | 11 | ## 0.1.2 (2020-10-11) 12 | 13 | * Minor updates to html documentation. 14 | 15 | ## 0.1.3 (2020-11-30) 16 | 17 | * Improvements to figures styles. 18 | 19 | ## 0.1.4 (2020-11-21) 20 | 21 | * Minor updates to html documentation. 22 | 23 | ## 0.1.5 (2020-12-26) 24 | 25 | * Minor updates to html documentation. 26 | 27 | ## 0.1.6 (2020-12-26) 28 | 29 | * Minor updates to html documentation and improvements to figures. 30 | 31 | ## 0.1.7 (2020-12-26) 32 | 33 | * Use mstools instead of scikit-learn for linear regression coefficient of determination. 34 | 35 | ## 0.1.8 (2021-06-28) 36 | 37 | * Minor improvements to figures. 38 | 39 | ## 0.1.9 (2022-08-24) 40 | 41 | * Update documentation. 42 | * Include feature to the ``Data`` class to read data with multiple unloading-reloading stages. 43 | * Remove dependency of external LaTeX installation. 44 | 45 | ## 0.1.10 (2023-10-31) 46 | * Include the ``range2fitCS`` to the ``getSigmaP`` method of the ``Casagrande`` class to limit the stress range for the cubic spline in the calculation of the maximum curvature point. 47 | * The maximum curvature point is now calculated using a function within the ``casagrande`` module 48 | that determines the maximum value of the curvature that is not at the ends, instead 49 | of the ``find_peaks`` SciPy's function. However, if the value is not determined, the maximum 50 | curvature point is calculated as the absolute maximum value of the curvature. 51 | * Improve some figures: 52 | 53 | - The void ratio of the :math:`\sigma'_\mathrm{v}` value is now determined by 54 | projecting :math:`\sigma'_\mathrm{v}` over a linear interpolation instead of a cubic spline. 55 | - :math:`C_\mathrm{c}` and :math:`C_\mathrm{r}` are not calculated inmediatelly when loading the data. 56 | Instead, they are calculated when theis respective methods are called. 57 | - Other minor improvements. 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | illustrative_examples/* 128 | .vscode/* 129 | docs_OLD/* 130 | -------------------------------------------------------------------------------- /illustrative_examples/illustrative_examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | illustrative_examples.py module. 3 | 4 | This script contains the code used to generate the figures related to the 5 | section 'Software Functionalities and Illustrative Examples' of the paper 6 | 'An Open Source Application Software to Determine the 7 | Preconsolidation Pressure of Soils in Incremental Loading Oedometer Testing: 8 | pySigmaP' by Exneyder A. Montoya-Araque, A. J. Aparicio-Ortube, David G. 9 | Zapata-Medina and Luis G. Arboleda-Monsalve. 10 | """ 11 | 12 | # Block 1: Input loading data from an external file 13 | from pysigmap.energy import WangAndFrost 14 | from pysigmap.energy import BeckerEtAl 15 | from pysigmap.bilog import Bilog 16 | from pysigmap.boone import Boone 17 | from pysigmap.pachecosilva import PachecoSilva 18 | from pysigmap.casagrande import Casagrande 19 | import pandas as pd 20 | from pysigmap.data import Data 21 | import matplotlib.pyplot as plt 22 | 23 | url = "".join( 24 | [ 25 | "https://raw.githubusercontent.com/", 26 | "eamontoyaa/data4testing/", 27 | "main/pysigmap/testData.csv", 28 | ] 29 | ) 30 | data = Data(pd.read_csv(url), sigmaV=75) 31 | fig = data.plot() # Figure 2a 32 | 33 | # Block 2: Cc and Cr calculated following published criteria 34 | # 2.1 - Default parameters: Cc (maximum slope) – Cr (opt=1) 35 | data.compressionIdx(range2fitCc=None) 36 | data.recompressionIdx(opt=1) 37 | fig = data.plot() # Figure 2a 38 | # 2.2: Cc (two last points) – Cr (opt=2) 39 | data.compressionIdx(range2fitCc=(3000, 8000)) 40 | data.recompressionIdx(opt=2) 41 | fig = data.plot() # Figure 2b 42 | # 2.3: Cc (four last points) – Cr (opt=3) 43 | data.compressionIdx(range2fitCc=(700, 8000)) 44 | data.recompressionIdx(opt=3) 45 | fig = data.plot() # Figure 2c 46 | 47 | # Block 3: Computation of 〖σ'〗_"p" via the Casagrande method 48 | method = Casagrande(data) 49 | # 3.1 - default parameters: cubic spline function 50 | fig = method.getSigmaP(mcp=None, range2fitFOP=None, loglog=True) # Figure 3a 51 | # 3.2: fourth order polynomial (FOP) 52 | fig = method.getSigmaP(range2fitFOP=[20, 5000], loglog=True) # Figure 3b 53 | # 3.3: MCP manually introduced 54 | fig = method.getSigmaP(mcp=200) # Not shown 55 | 56 | # Block 4: Computation of 〖σ'〗_"p" via the Pacheco Silva and Boone methods 57 | # 4.1: Pacheco Silva method 58 | method = PachecoSilva(data) 59 | fig = method.getSigmaP() # Figure 3c 60 | # 4.2: Boone method 61 | method = Boone(data) 62 | fig = method.getSigmaP() # Figure 3d 63 | 64 | # Block 5: Computation of 〖σ'〗_"p" via the bilogarithmic methods 65 | method = Bilog(data) 66 | # 5.1: Butterfield method 67 | fig = method.getSigmaP(range2fitRR=None, range2fitCR=None, opt=1) # Figure 4a 68 | 69 | # 5.2: Oikawa method 70 | fig = method.getSigmaP( 71 | range2fitRR=None, range2fitCR=[1000, 5000], opt=2 72 | ) # Figure 4b 73 | # 5.3: Onitsuka et al. method 74 | fig = method.getSigmaP( 75 | range2fitRR=[0, 30], range2fitCR=[1000, 9000], opt=3 76 | ) # Figure 4c 77 | 78 | # Block 6: Computation of 〖σ'〗_"p" via the strain energy methods 79 | method = BeckerEtAl(data) 80 | # 6.1: Becker et al. method 81 | fig = method.getSigmaP( 82 | range2fitRR=None, range2fitCR=None, morinFormulation=False, zoom=5.5 83 | ) # Figure 5a 84 | # 6.2: Morin method 85 | fig = method.getSigmaP( 86 | range2fitRR=[0, 100], 87 | range2fitCR=[700, 9000], 88 | morinFormulation=True, 89 | zoom=5.5, 90 | ) # Figure 5b 91 | # 6.3: Wang and Frost method 92 | method = WangAndFrost(data) 93 | fig = method.getSigmaP(range2fitCR=None) # Figure 5c 94 | 95 | plt.show() 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pySigmaP` 2 | 3 | [![made-with-python](https://img.shields.io/badge/Made_with-Python3-306998?style=badge&logo=python&logoColor=white)](https://github.com/eamontoyaa/pySigmaP) [![PyPI repo](https://img.shields.io/pypi/v/pysigmap.svg)](https://pypi.org/project/pysigmap) [![BSD-2 License](https://img.shields.io/badge/License-BSD2-FFAB00?style=badge&logo=opensourceinitiative&logoColor=white)](https://opensource.org/license/bsd-2-clause/) [![Docs](https://readthedocs.org/projects/pysigmap/badge/?version=latest)](https://pysigmap.readthedocs.io/en/latest/?badge=latest) 4 | 5 | ## Description 6 | 7 | **``pySigmaP``** is an application software developed in Python3 to determine 8 | the preconsolidation pressure of soils in incremental loading (IL) oedometer 9 | testing. **``pySigmaP``** includes nine methods such as the method of Casagrande, 10 | Pacheco Silva, Butterfield, Oikawa, Becker et al., Morin, Onitsuka et al., 11 | Wang and Frost, and Boone. 12 | 13 | In this repo and in the package [documentation](https://pysigmap.readthedocs.io) you will find the source code, instructions for installation, docstrings, examples of use, development history track, and references. 14 | 15 | ## Installation 16 | 17 | It is suggested to create a virtual environment to install and use the program. 18 | 19 | ### Stable release 20 | 21 | We recommend installing **``pySigmaP``** from [PyPI](https://pypi.org/project/pySigmaP), as it will always install the most recent stable release. To do so, run this command in your terminal: 22 | 23 | pip install pysigmap 24 | 25 | ### From sources 26 | 27 | The sources for **``pySigmaP``** can be downloaded from the [Github repo](https://github.com/eamontoyaa/pySigmaP). You can clone the public repository running the following command: 28 | 29 | git clone git://github.com/eamontoyaa/pySigmaP 30 | 31 | Once you have a copy of the source code, you can install it with the following instruction: 32 | 33 | pip install -e . 34 | 35 | ### Dependencies 36 | 37 | The code was written in Python 3.9. The packages `numpy`, `matplotlib`, `scipy`, `pandas` and `mstools` are required for using **``pySigmaP``**. They should be installed along with the package, however, all of them can also be manually installed from the PyPI repository by opening a terminal and typing the following code lines: 38 | 39 | pip install numpy==1.23.5 40 | pip install matplotlib==3.7.1 41 | pip install scipy==1.11.3 42 | pip install pandas==1.5.3 43 | pip install mstools==0.1.0 44 | 45 | 46 | ## Authorship and Citation 47 | 48 | The team behind **``pySigmaP``** includes: 49 | 50 | **Development Team** 51 | 52 | * Exneyder A. Montoya-Araque, Geol.Eng., MEng., 53 | * Alan J. Aparicio-Ortube, Civ.Eng., MSc., PhD. Student, 54 | 55 | **Contributors/Advisors** 56 | 57 | * David G. Zapata-Medina, Civ.Eng., MSc., PhD. 58 | * Luis G. Arboleda-Monsalve, Civ.Eng., MSc., PhD. 59 | 60 | 61 | To cite **``pySigmaP``** in publications, use the following reference: 62 | 63 | Montoya-Araque et al. (2022). An open-source application software to determine the preconsolidation pressure of soils in incremental loading oedometer testing: pySigmaP. SoftwareX, 17, 100990. https://doi.org/10.1016/j.softx.2022.100990 64 | 65 | A BibTeX entry for LaTeX users is: 66 | 67 | ``` bibtex 68 | @article{MontoyaAraque_etal_2022_art, 69 | author = {Montoya-Araque, Exneyder A. and Aparicio-Ortube, A.J. and Zapata-Medina, David G. and Arboleda-Monsalve, Luis G.}, 70 | doi = {10.1016/j.softx.2022.100990}, 71 | issn = {23527110}, 72 | journal = {SoftwareX}, 73 | keywords = {Oedometer testing,Open-source application software,Preconsolidation pressure,Python 3,Soil}, 74 | month = {jan}, 75 | pages = {100990}, 76 | publisher = {Elsevier B.V.}, 77 | title = {{An open-source application software to determine the preconsolidation pressure of soils in incremental loading oedometer testing: pySigmaP}}, 78 | volume = {17}, 79 | year = {2022} 80 | } 81 | ``` 82 | 83 | ## License 84 | 85 | **``pySigmaP``** is a free and open-source application sorfware licensed under the terms of the [BSD-2-Clause License](https://opensource.org/licenses/BSD-2-Clause). 86 | -------------------------------------------------------------------------------- /pysigmap/pachecosilva.py: -------------------------------------------------------------------------------- 1 | """ ``pachecosilva.py`` module. 2 | 3 | Contains the class and its methods to determine the preconsolidation 4 | pressure from a compressibility curve via the method proposed by Pacheco Silva 5 | (1970). 6 | 7 | """ 8 | 9 | # -- Required modules 10 | import numpy as np 11 | from scipy.interpolate import CubicSpline, interp1d 12 | import matplotlib as mpl 13 | import matplotlib.pyplot as plt 14 | import matplotlib.ticker as mtick 15 | 16 | from pysigmap import figsize, colors 17 | 18 | mpl.rcParams.update( 19 | { 20 | "text.usetex": False, # Use mathtext, not LaTeX 21 | "font.family": "serif", # Use the Computer modern font 22 | "font.serif": "cmr10", 23 | "mathtext.fontset": "cm", 24 | "axes.formatter.use_mathtext": True, 25 | "axes.unicode_minus": False, 26 | } 27 | ) 28 | 29 | 30 | class PachecoSilva: 31 | """``PachecoSilva`` class. 32 | 33 | When the object is instanced, the method ``getSigmaP()`` calculates the 34 | preconsolidation pressure by the method proposed by Pacheco Silva (1970) 35 | based on the compression index obtained with the ``Data`` class. 36 | 37 | Attributes 38 | ---------- 39 | data : Object instanced from the ``Data`` class. 40 | Contains the data structure from the oedometer test. See the class 41 | documentation for more information. 42 | 43 | Examples 44 | -------- 45 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 46 | >>> 'data4testing/main/pysigmap/testData.csv']) 47 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 48 | >>> method = PachecoSilva(data) 49 | >>> method.getSigmaP() 50 | >>> method.sigmaP, method.ocr 51 | (330.63741795901075, 4.408498906120143) 52 | """ 53 | 54 | def __init__(self, data): 55 | """Initialize the class.""" 56 | self.data = data 57 | return 58 | 59 | def getSigmaP(self): 60 | """ 61 | Return the value of the preconsolidation pressure. 62 | 63 | Returns 64 | ------- 65 | fig : matplotlib figure 66 | Figure with the development of the method and the results. 67 | 68 | """ 69 | # -- Cubic spline that passes through the data 70 | sigmaLog = np.log10(self.data.cleaned["stress"][1:]) 71 | cs = CubicSpline(x=sigmaLog, y=self.data.cleaned["e"][1:]) 72 | interpolator = interp1d(x=sigmaLog, y=self.data.cleaned["e"][1:]) 73 | 74 | # -- Lines of the Pacheco Silva's method 75 | # Intersection: NCL - e_0 76 | xPt1 = 10 ** ((self.data.e_0 - self.data.idxCcInt) / -self.data.idxCc) 77 | # Intersection: first vertical - compressibility curve 78 | # yPt2 = cs(np.log10(xPt1)) 79 | yPt2 = interpolator(np.log10(xPt1)) 80 | # Intersection: first horizontal - NCL (Preconsolidation pressure) 81 | self.sigmaP = 10 ** ((yPt2 - self.data.idxCcInt) / -self.data.idxCc) 82 | self.ocr = self.sigmaP / self.data.sigmaV 83 | self.eSigmaP = yPt2 84 | 85 | # -- plotting 86 | fig = plt.figure(figsize=figsize) 87 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 88 | ax.plot( 89 | self.data.raw["stress"][1:], 90 | self.data.raw["e"][1:], 91 | ls=(0, (1, 1)), 92 | marker="o", 93 | lw=1.5, 94 | c="k", 95 | mfc="w", 96 | label="Compressibility curve", 97 | ) 98 | ax.hlines( 99 | y=self.data.e_0, 100 | xmin=0, 101 | xmax=xPt1, 102 | ls="--", 103 | lw=1.125, 104 | color=colors[2], 105 | label=f"$e_0 = {self.data.e_0:.3f}$", 106 | ) 107 | # Compression index 108 | x4Cc = np.linspace(xPt1, self.data.cleaned["stress"].iloc[-1]) 109 | y4Cc = -self.data.idxCc * np.log10(x4Cc) + self.data.idxCcInt 110 | ax.plot( 111 | x4Cc, 112 | y4Cc, 113 | ls="-", 114 | lw=1.125, 115 | color=colors[1], 116 | label=str().join(["$C_\mathrm{c}=$", f"{self.data.idxCc:.3f}"]), 117 | ) 118 | if self.data.fitCc: 119 | ax.plot( 120 | self.data.cleaned["stress"].iloc[self.data.maskCc], 121 | self.data.cleaned["e"].iloc[self.data.maskCc], 122 | ls="", 123 | marker="x", 124 | lw=1.5, 125 | color=colors[1], 126 | label=f"Data for linear fit\n(R$^2={self.data.r2Cc:.3f}$)", 127 | ) 128 | # Lines of the Pacheco-Silva's method 129 | ax.vlines( 130 | x=xPt1, ymin=yPt2, ymax=self.data.e_0, lw=1.125, color="k", ls="--" 131 | ) 132 | ax.hlines( 133 | y=yPt2, xmin=xPt1, xmax=self.sigmaP, lw=1.125, color="k", ls="--" 134 | ) 135 | # ax.vlines(x=self.sigmaP, ymin=self.data.raw['e'].min(), ymax=yPt2, 136 | # lw=1.5, color='k', ls='-.') 137 | # Other plots 138 | ax.plot( 139 | self.data.sigmaV, 140 | self.data.eSigmaV, 141 | ls="", 142 | marker="|", 143 | c="r", 144 | ms=15, 145 | mfc="w", 146 | mew=1.5, 147 | label=str().join( 148 | [ 149 | "$\sigma^\prime_\mathrm{v_0}=$ ", 150 | f"{self.data.sigmaV:.0f} kPa", 151 | ] 152 | ), 153 | ) 154 | ax.plot( 155 | self.sigmaP, 156 | self.eSigmaP, 157 | ls="", 158 | marker="o", 159 | c=colors[0], 160 | ms=7, 161 | mfc="w", 162 | mew=1.5, 163 | label=str().join( 164 | [ 165 | "$\sigma^\prime_\mathrm{p}=$ ", 166 | f"{self.sigmaP:.0f} kPa\n", 167 | f"OCR= {self.ocr:.1f}", 168 | ] 169 | ), 170 | ) 171 | # Other details 172 | ax.spines["top"].set_visible(False) 173 | ax.spines["right"].set_visible(False) 174 | ax.set( 175 | xscale="log", 176 | ylabel="Void ratio, $e$", 177 | xlabel=str().join( 178 | [ 179 | "Effective vertical stress, ", 180 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 181 | ] 182 | ), 183 | ) 184 | ax.xaxis.set_major_formatter(mtick.ScalarFormatter()) 185 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 186 | ax.grid(False) 187 | ax.legend( 188 | bbox_to_anchor=(1.125, 0.5), 189 | loc=6, 190 | title="$\\bf{Pacheco\ Silva\ method}$", 191 | ) 192 | return fig 193 | 194 | 195 | # %% 196 | """ 197 | 2-Clause BSD License. 198 | 199 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 200 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 201 | UNIVERSIDAD NACIONAL DE COLOMBIA. 202 | 203 | Redistribution and use in source and binary forms, with or without 204 | modification, are permitted provided that the following conditions are met: 205 | 206 | 1. Redistributions of source code must retain the above copyright notice, this 207 | list of conditions and the following disclaimer. 208 | 209 | 2. Redistributions in binary form must reproduce the above copyright notice, 210 | this list of conditions and the following disclaimer in the documentation 211 | and/or other materials provided with the distribution. 212 | 213 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 214 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 215 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 216 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 217 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 218 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 219 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 220 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 221 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 222 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 223 | """ 224 | -------------------------------------------------------------------------------- /pysigmap/boone.py: -------------------------------------------------------------------------------- 1 | """ ``boone.py`` module. 2 | 3 | Contains the class and its methods to determine the preconsolidation 4 | pressure from a compressibility curve via the method proposed by 5 | [Boone (2010)](https://doi.org/10.1139/T09-093). 6 | 7 | """ 8 | 9 | # -- Required modules 10 | import numpy as np 11 | from numpy.polynomial.polynomial import polyval 12 | import matplotlib as mpl 13 | import matplotlib.pyplot as plt 14 | import matplotlib.ticker as mtick 15 | 16 | from pysigmap import figsize, colors 17 | 18 | mpl.rcParams.update( 19 | { 20 | "text.usetex": False, # Use mathtext, not LaTeX 21 | "font.family": "serif", # Use the Computer modern font 22 | "font.serif": "cmr10", 23 | "mathtext.fontset": "cm", 24 | "axes.formatter.use_mathtext": True, 25 | "axes.unicode_minus": False, 26 | } 27 | ) 28 | 29 | 30 | class Boone: 31 | """``Boone`` class. 32 | 33 | When the object is instanced, the method ``getSigmaP()`` calculates the 34 | preconsolidation pressure by the method proposed by Boone (2010) based on 35 | the compression and recompression indices obtained with the ``Data`` class. 36 | 37 | Attributes 38 | ---------- 39 | data : Object instanced from the ``Data`` class. 40 | Contains the data structure from the oedometer test. See the class 41 | documentation for more information. 42 | 43 | Examples 44 | -------- 45 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 46 | >>> 'data4testing/main/pysigmap/testData.csv']) 47 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 48 | >>> method = Boone(data) 49 | >>> method.getSigmaP() 50 | >>> method.sigmaP, method.ocr 51 | (384.38457280143547, 5.125127637352473) 52 | """ 53 | 54 | def __init__(self, data): 55 | """Initialize the class.""" 56 | self.data = data 57 | return 58 | 59 | def getSigmaP(self): 60 | """ 61 | Return the value of the preconsolidation pressure. 62 | 63 | Returns 64 | ------- 65 | fig : matplotlib figure 66 | Figure with the development of the method and the results. 67 | """ 68 | # Intercept of the line parallel to Cr passing through (σ_v0, e_σ_v0) 69 | idxCr2Int = self.data.eSigmaV + self.data.idxCr * np.log10( 70 | self.data.sigmaV 71 | ) 72 | # Intersection of Line parallel to Cr2 - Cc (Preconsolidation pressure) 73 | self.sigmaP = 10 ** ( 74 | (self.data.idxCcInt - idxCr2Int) 75 | / (-self.data.idxCr + self.data.idxCc) 76 | ) 77 | self.eSigmaP = polyval( 78 | np.log10(self.sigmaP), [self.data.idxCcInt, -self.data.idxCc] 79 | ) 80 | self.ocr = self.sigmaP / self.data.sigmaV 81 | 82 | # -- plotting 83 | fig = plt.figure(figsize=figsize) 84 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 85 | ax.plot( 86 | self.data.raw["stress"][1:], 87 | self.data.raw["e"][1:], 88 | ls=(0, (1, 1)), 89 | marker="o", 90 | lw=1.5, 91 | c="k", 92 | mfc="w", 93 | label="Compressibility curve", 94 | ) 95 | # Compression index (Cc) 96 | x4Cc = np.linspace(self.sigmaP, self.data.cleaned["stress"].iloc[-1]) 97 | y4Cc = -self.data.idxCc * np.log10(x4Cc) + self.data.idxCcInt 98 | ax.plot( 99 | x4Cc, 100 | y4Cc, 101 | ls="-", 102 | lw=1.125, 103 | color=colors[1], 104 | label=str().join(["$C_\mathrm{c}=$", f"{self.data.idxCc:.3f}"]), 105 | ) 106 | if self.data.fitCc: 107 | ax.plot( 108 | self.data.cleaned["stress"].iloc[self.data.maskCc], 109 | self.data.cleaned["e"].iloc[self.data.maskCc], 110 | ls="", 111 | marker="x", 112 | color=colors[1], 113 | label=f"Data for linear fit\n(R$^2={self.data.r2Cc:.3f}$)", 114 | ) 115 | # Recompression index (Cr) 116 | x4Cr = np.linspace( 117 | self.data.raw["stress"].iloc[self.data.maskCr].min(), 118 | self.data.raw["stress"].iloc[self.data.maskCr].max(), 119 | ) 120 | y4Cr = -self.data.idxCr * np.log10(x4Cr) + self.data.idxCrInt 121 | ax.plot( 122 | x4Cr, 123 | y4Cr, 124 | ls="-", 125 | lw=1.125, 126 | color=colors[2], 127 | label=str().join(["$C_\mathrm{r}=$", f"{self.data.idxCr:.3f}"]), 128 | ) 129 | ax.plot( 130 | self.data.raw["stress"].iloc[self.data.maskCr], 131 | self.data.raw["e"].iloc[self.data.maskCr], 132 | ls="", 133 | marker="+", 134 | color=colors[2], 135 | label=f"Data for linear fit\n(R$^2={self.data.r2Cr:.3f}$)", 136 | ) 137 | # Line pararel to Cr 138 | x4Cr2 = np.linspace(self.data.cleaned["stress"].iloc[1], self.sigmaP) 139 | y4Cr2 = polyval(np.log10(x4Cr2), [idxCr2Int, -self.data.idxCr]) 140 | ax.plot( 141 | x4Cr2, 142 | y4Cr2, 143 | ls="--", 144 | c=colors[2], 145 | lw=1.125, 146 | label="Parallel line to $C_\\mathrm{r}$", 147 | ) 148 | # Other plots 149 | ax.plot( 150 | self.data.sigmaV, 151 | self.data.eSigmaV, 152 | ls="", 153 | marker="|", 154 | c="r", 155 | ms=15, 156 | mfc="w", 157 | mew=1.5, 158 | label=str().join( 159 | [ 160 | "$\sigma^\prime_\mathrm{v_0}=$ ", 161 | f"{self.data.sigmaV:.0f} kPa", 162 | ] 163 | ), 164 | ) 165 | ax.plot( 166 | self.sigmaP, 167 | self.eSigmaP, 168 | ls="", 169 | marker="o", 170 | c=colors[0], 171 | ms=7, 172 | mfc="w", 173 | mew=1.5, 174 | label=str().join( 175 | [ 176 | "$\sigma^\prime_\mathrm{p}=$ ", 177 | f"{self.sigmaP:.0f} kPa\n", 178 | f"OCR= {self.ocr:.1f}", 179 | ] 180 | ), 181 | ) 182 | # Other details 183 | ax.spines["top"].set_visible(False) 184 | ax.spines["right"].set_visible(False) 185 | ax.set( 186 | xscale="log", 187 | ylabel="Void ratio, $e$", 188 | xlabel=str().join( 189 | [ 190 | "Effective vertical stress, ", 191 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 192 | ] 193 | ), 194 | ) 195 | ax.xaxis.set_major_formatter(mtick.ScalarFormatter()) 196 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 197 | ax.grid(False) 198 | ax.legend( 199 | bbox_to_anchor=(1.125, 0.5), loc=6, title="$\\bf{Boone\ method}$" 200 | ) 201 | return fig 202 | 203 | 204 | # %% 205 | """ 206 | 2-Clause BSD License. 207 | 208 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 209 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 210 | UNIVERSIDAD NACIONAL DE COLOMBIA. 211 | 212 | Redistribution and use in source and binary forms, with or without 213 | modification, are permitted provided that the following conditions are met: 214 | 215 | 1. Redistributions of source code must retain the above copyright notice, this 216 | list of conditions and the following disclaimer. 217 | 218 | 2. Redistributions in binary form must reproduce the above copyright notice, 219 | this list of conditions and the following disclaimer in the documentation 220 | and/or other materials provided with the distribution. 221 | 222 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 223 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 224 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 225 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 226 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 227 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 228 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 229 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 230 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 231 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 232 | """ 233 | -------------------------------------------------------------------------------- /pysigmap/bilog.py: -------------------------------------------------------------------------------- 1 | """``bilog.py`` module. 2 | 3 | Contains the class and its methods to determine the preconsolidation 4 | pressure from a compressibility curve via the Bilogarithmic methods proposed 5 | by [Butterfield (1979)](https://doi.org/10.1680/geot.1979.29.4.469), 6 | [Oikawa (1987)](https://doi.org/10.3208/sandf1972.27.3_99) and 7 | [Onitsuka et al. (1995)](https://doi.org/10.3208/sandf.35.61). 8 | 9 | """ 10 | 11 | # -- Required modules 12 | import numpy as np 13 | from numpy.polynomial.polynomial import polyfit, polyval 14 | from scipy.interpolate import CubicSpline, interp1d 15 | import matplotlib as mpl 16 | import matplotlib.pyplot as plt 17 | import matplotlib.ticker as mtick 18 | from mstools.mstools import r2_score 19 | 20 | from pysigmap import figsize, colors 21 | 22 | mpl.rcParams.update( 23 | { 24 | "text.usetex": False, # Use mathtext, not LaTeX 25 | "font.family": "serif", # Use the Computer modern font 26 | "font.serif": "cmr10", 27 | "mathtext.fontset": "cm", 28 | "axes.formatter.use_mathtext": True, 29 | "axes.unicode_minus": False, 30 | } 31 | ) 32 | 33 | 34 | class Bilog: 35 | """``Bilog`` class. 36 | 37 | When the object is instanced, the method ``getSigmaP()`` calculates the 38 | preconsolidation pressure by the methods proposed by Butterfield (1979), 39 | Oikawa (1987) and Onitsuka et al. (1995) based on the parameters of the 40 | method. See the method documentation for more information. 41 | 42 | Attributes 43 | ---------- 44 | data : Object instanced from the ``Data`` class 45 | Contains the data structure from the oedometer test. See the class 46 | documentation for more information. 47 | 48 | Examples 49 | -------- 50 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 51 | >>> 'data4testing/main/pysigmap/testData.csv']) 52 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 53 | >>> method = Bilog(data) 54 | >>> method.getSigmaP(opt=1) # Butterfield (1979) 55 | >>> method.sigmaP, method.ocr 56 | (433.29446692547555, 5.777259559006341) 57 | >>> method.getSigmaP(range2fitCR=[1000, 9000], opt=1) # Butterfield 58 | >>> method.sigmaP, method.ocr 59 | (399.3361458736937, 5.324481944982582) 60 | >>> method.getSigmaP(opt=2) # Oikawa (1987) 61 | >>> method.sigmaP, method.ocr 62 | (433.2944669254731, 5.777259559006308) 63 | >>> method.getSigmaP(range2fitCR=[1000, 9000], opt=2) # Oikawa 64 | >>> method.sigmaP, method.ocr 65 | (399.3361458736935, 5.32448194498258) 66 | >>> method.getSigmaP(opt=3) # Onitsuka et al. (1995) 67 | >>> method.sigmaP, method.ocr 68 | (433.2944669254753, 5.777259559006338) 69 | >>> method.getSigmaP(range2fitCR=[1000, 9000], opt=3) # Onitsuka et al. 70 | >>> method.sigmaP, method.ocr 71 | (399.3361458736939, 5.324481944982585) 72 | """ 73 | 74 | def __init__(self, data): 75 | """Initialize the class.""" 76 | self.data = data 77 | return 78 | 79 | def getSigmaP(self, range2fitRR=None, range2fitCR=None, opt=1): 80 | """ 81 | Return the value of the preconsolidation pressure. 82 | 83 | Parameters 84 | ---------- 85 | range2fitRR : list, tuple or array (length=2), optional 86 | Initial and final pressures between which the first order 87 | polynomial will be fitted to the data on the recompression range 88 | (RR) (range before the preconsolidation pressure). If None, the 89 | first order polynomial will be fitted from the first point of the 90 | curve to the point before the in situ effective vertical stress. 91 | The default is None. 92 | range2fitCR : list, tuple or array (length=2), optional 93 | Initial and final pressures between which the first order 94 | polynomial will be fitted to the data on the compression range (CR) 95 | (range above the preconsolidation pressure). If None, the CR will 96 | be automatically fitted to the same points used for calculating the 97 | compression index with the ``Data`` class only if it was calculated 98 | with a linear fit, otherwise, the steepest slope of the cubic 99 | spline that passes through the data will be used. The default is 100 | None. 101 | opt : int, optional 102 | Integer value to indicate which bilogarithmic method will be used. 103 | Butterfield (opt=1), Oikawa (opt=2) and Onitsuka et al. (opt=3). 104 | The default is 1. 105 | 106 | Returns 107 | ------- 108 | fig : matplotlib.figure.Figure 109 | Figure with the development of the method and the results. 110 | 111 | """ 112 | 113 | def transformX(x, opt=1): 114 | return np.log(x) if opt == 1 else np.log10(x) 115 | 116 | def transformY(y, opt=1): 117 | return np.log10(y) if opt == 2 else np.log(y) 118 | 119 | self.data.raw["vol"] = self.data.raw["e"] + 1 120 | self.data.clean() # -- Updating data without unloads 121 | 122 | # def ticks(x, pos): return f'$e^{np.log(x):.0f}$' 123 | 124 | if range2fitRR is None: # Indices for fitting the RR line 125 | idxInitRR = 1 126 | idxEndRR = ( 127 | self.data.findStressIdx( 128 | stress2find=self.data.sigmaV, cleanedData=True 129 | ) 130 | - 1 131 | ) 132 | else: 133 | idxInitRR = self.data.findStressIdx( 134 | stress2find=range2fitRR[0], cleanedData=True 135 | ) 136 | idxEndRR = ( 137 | self.data.findStressIdx( 138 | stress2find=range2fitRR[1], cleanedData=True 139 | ) 140 | - 1 141 | ) 142 | 143 | # -- Linear regresion of points on the recompression range(RR) 144 | sigmaRR = self.data.cleaned["stress"][idxInitRR : idxEndRR + 1] 145 | volRR = self.data.cleaned["vol"][idxInitRR : idxEndRR + 1] 146 | sigmaRRlog = transformX(sigmaRR, opt) 147 | volRRlog = transformY(volRR, opt) 148 | p1_0, p1_1 = polyfit(sigmaRRlog, volRRlog, deg=1) 149 | r2RR = r2_score( 150 | y_true=volRRlog, y_pred=polyval(sigmaRRlog, [p1_0, p1_1]) 151 | ) 152 | 153 | # -- Cubic spline that passes through the data 154 | sigmaLog = transformX(self.data.cleaned["stress"][1:], opt) 155 | volLog = transformY(self.data.cleaned["vol"][1:], opt) 156 | cs = CubicSpline(x=sigmaLog, y=volLog) 157 | # Specific volume at sigma V 158 | # volSigmaV = cs(transformX(self.data.sigmaV, opt)) 159 | interpolator = interp1d(x=sigmaLog, y=volLog) 160 | volSigmaV = interpolator(transformX(self.data.sigmaV, opt)) 161 | 162 | # -- Compression range (CR) 163 | self.maskCR = np.full(len(self.data.cleaned), False) 164 | if range2fitCR is not None or self.data.fitCc: # Using a linear fit 165 | if range2fitCR is not None: 166 | idxInitCR = self.data.findStressIdx( 167 | stress2find=range2fitCR[0], cleanedData=True 168 | ) 169 | idxEndCR = self.data.findStressIdx( 170 | stress2find=range2fitCR[1], cleanedData=True 171 | ) 172 | self.maskCR[idxInitCR:idxEndCR] = True 173 | elif self.data.fitCc: 174 | self.maskCR = self.data.maskCc 175 | # -- Linear regresion of points on post yield line 176 | sigmaCR = self.data.cleaned["stress"][self.maskCR] 177 | sigmaCRlog = transformX(sigmaCR, opt) 178 | volCR = self.data.cleaned["vol"][self.maskCR] 179 | volCRlog = transformY(volCR, opt) 180 | lcrInt, lcrSlope = polyfit(sigmaCRlog, volCRlog, deg=1) 181 | r2CR = r2_score( 182 | y_true=volCRlog, y_pred=polyval(sigmaCRlog, [lcrInt, lcrSlope]) 183 | ) 184 | 185 | else: # Using the steepest point of a cubic spline 186 | sigmaCS = np.linspace(sigmaLog.iloc[0], sigmaLog.iloc[-1], 500) 187 | steepestSlopeIdx = np.argmin(cs(sigmaCS, 1)) 188 | lcrSlope = cs(sigmaCS, 1)[steepestSlopeIdx] 189 | lcrInt = ( 190 | cs(sigmaCS[steepestSlopeIdx]) 191 | - lcrSlope * sigmaCS[steepestSlopeIdx] 192 | ) 193 | 194 | xScl = np.e if opt == 1 else 10 # log scale for plotting 195 | yLabel = "$\log_{10} (1+e)$" if opt == 2 else "$\ln (1+e)$" 196 | 197 | # -- Preconsolitadion pressure 198 | self.sigmaP = xScl ** ((lcrInt - p1_0) / (p1_1 - lcrSlope)) 199 | self.vSigmaP = polyval(transformX(self.sigmaP, opt), [p1_0, p1_1]) 200 | self.ocr = self.sigmaP / self.data.sigmaV 201 | 202 | # -- Lines of the bilogarithmic methods 203 | xCR = np.linspace(self.sigmaP, self.data.cleaned["stress"].iloc[-1]) 204 | yCR = polyval(transformX(xCR, opt), [lcrInt, lcrSlope]) 205 | 206 | # Recompression range 207 | xRRFit = np.linspace(sigmaRR.iloc[0], self.sigmaP) 208 | yRRFit = polyval(transformX(xRRFit, opt), [p1_0, p1_1]) 209 | 210 | # -- plot compresibility curve 211 | fig = plt.figure(figsize=figsize) 212 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 213 | ax.semilogx( 214 | self.data.raw["stress"][1:], 215 | transformY(self.data.raw["vol"][1:], opt), 216 | base=xScl, 217 | ls=(0, (1, 1)), 218 | marker="o", 219 | lw=1.5, 220 | c="k", 221 | mfc="w", 222 | label="Compressibility curve", 223 | ) 224 | methods = ["Butterfield", "Oikawa", "Onitsuka\ et\ al."] 225 | # Recompression range 226 | ax.plot( 227 | xRRFit, 228 | yRRFit, 229 | ls="-", 230 | c=colors[2], 231 | lw=1.125, 232 | label="Recompression range", 233 | ) 234 | ax.plot( 235 | sigmaRR, 236 | volRRlog, 237 | ls="", 238 | marker="+", 239 | c=colors[2], 240 | label=f"Data for linear fit\n(R$^2={r2RR:.3f}$)", 241 | ) 242 | # Compression range 243 | ax.plot( 244 | xCR, yCR, ls="-", c=colors[1], lw=1.125, label="Compression range" 245 | ) 246 | if range2fitCR is not None or self.data.fitCc: 247 | ax.plot( 248 | sigmaCR, 249 | volCRlog, 250 | ls="", 251 | marker="x", 252 | c=colors[1], 253 | label=f"Data for linear fit\n(R$^2={r2CR:.3f}$)", 254 | ) 255 | # Other plots 256 | ax.plot( 257 | self.data.sigmaV, 258 | volSigmaV, 259 | ls="", 260 | marker="|", 261 | c="r", 262 | ms=15, 263 | mfc="w", 264 | mew=1.5, 265 | label=str().join( 266 | [ 267 | "$\sigma^\prime_\mathrm{v0}=$ ", 268 | f"{self.data.sigmaV:.0f} kPa", 269 | ] 270 | ), 271 | ) 272 | ax.plot( 273 | self.sigmaP, 274 | self.vSigmaP, 275 | ls="", 276 | marker="o", 277 | c=colors[0], 278 | ms=7, 279 | mfc="w", 280 | mew=1.5, 281 | label=str().join( 282 | [ 283 | "$\sigma^\prime_\mathrm{p}=$ ", 284 | f"{self.sigmaP:.0f} kPa\n", 285 | f"OCR= {self.ocr:.1f}", 286 | ] 287 | ), 288 | ) 289 | # Other details 290 | ax.spines["top"].set_visible(False) 291 | ax.spines["right"].set_visible(False) 292 | ax.set( 293 | ylabel=f"Specific volume, {yLabel}", 294 | xlabel=str().join( 295 | [ 296 | "Effective vertical stress, ", 297 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 298 | ] 299 | ), 300 | ) 301 | ax.xaxis.set_major_formatter(mtick.ScalarFormatter()) 302 | # ax.xaxis.set_minor_locator(mtick.AutoMinorLocator()) 303 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 304 | ax.grid(False) 305 | ax.legend( 306 | bbox_to_anchor=(1.125, 0.5), 307 | loc=6, 308 | title=str().join(["$\\bf{", f"{methods[opt-1]}", "\ method}$"]), 309 | ) 310 | return fig 311 | 312 | 313 | # %% 314 | """ 315 | 2-Clause BSD License. 316 | 317 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 318 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 319 | UNIVERSIDAD NACIONAL DE COLOMBIA. 320 | 321 | Redistribution and use in source and binary forms, with or without 322 | modification, are permitted provided that the following conditions are met: 323 | 324 | 1. Redistributions of source code must retain the above copyright notice, this 325 | list of conditions and the following disclaimer. 326 | 327 | 2. Redistributions in binary form must reproduce the above copyright notice, 328 | this list of conditions and the following disclaimer in the documentation 329 | and/or other materials provided with the distribution. 330 | 331 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 332 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 333 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 334 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 335 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 336 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 337 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 338 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 339 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 340 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 341 | """ 342 | -------------------------------------------------------------------------------- /pysigmap/casagrande.py: -------------------------------------------------------------------------------- 1 | """``casagrande.py`` module. 2 | 3 | Contains the class and its methods to determine the preconsolidation 4 | pressure from a compressibility curve via the method proposed by Casagrande 5 | (1963). 6 | 7 | """ 8 | 9 | # -- Required modules 10 | import numpy as np 11 | from numpy.polynomial.polynomial import polyfit, polyval 12 | from scipy.interpolate import CubicSpline 13 | from scipy.signal import find_peaks 14 | import matplotlib as mpl 15 | import matplotlib.pyplot as plt 16 | import matplotlib.ticker as mtick 17 | from mstools.mstools import r2_score 18 | 19 | from pysigmap import figsize, colors 20 | 21 | mpl.rcParams.update( 22 | { 23 | "text.usetex": False, # Use mathtext, not LaTeX 24 | "font.family": "serif", # Use the Computer modern font 25 | "font.serif": "cmr10", 26 | "mathtext.fontset": "cm", 27 | "axes.formatter.use_mathtext": True, 28 | "axes.unicode_minus": False, 29 | } 30 | ) 31 | 32 | 33 | class Casagrande: 34 | """``Casagrande`` class. 35 | 36 | When the object is instanced, the method ``getSigmaP()`` calculates the 37 | preconsolidation pressure by the method proposed by Casagrande (1963) 38 | based on the compression index obtained with the ``Data`` class and the 39 | parameters of the method. See the method documentation for more 40 | information. 41 | 42 | Attributes 43 | ---------- 44 | data : Object instanced from the ``Data`` class. 45 | Contains the data structure from the oedometer test. See the class 46 | documentation for more information. 47 | 48 | Examples 49 | -------- 50 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 51 | >>> 'data4testing/main/pysigmap/testData.csv']) 52 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 53 | >>> method = Casagrande(data) 54 | >>> method.getSigmaP(range2fitFOP=None, loglog=True) 55 | >>> method.sigmaP, method.ocr 56 | (925.6233444375316, 12.34164459250042) 57 | >>> method.getSigmaP(range2fitFOP=[20, 5000], loglog=True) 58 | >>> method.sigmaP, method.ocr 59 | (651.5675456910274, 8.687567275880365) 60 | >>> method.getSigmaP(mcp=200) 61 | >>> method.sigmaP, method.ocr 62 | (498.6050168481866, 6.6480668913091545) 63 | """ 64 | 65 | def __init__(self, data): 66 | """Initialize the class.""" 67 | self.data = data 68 | return 69 | 70 | def getSigmaP( 71 | self, mcp=None, range2fitFOP=None, range2fitCS=None, loglog=True 72 | ): 73 | """ 74 | Return the value of the preconsolidation pressure. 75 | 76 | Parameters 77 | ---------- 78 | mcp : float, optional 79 | Variable to manually specify the maximun curvature point. If a 80 | value is input, the other parameters are not taken into account. 81 | The default is None. 82 | range2fitFOP : list, tuple or array (length=2), optional 83 | Initial and final pressures between which the fourth-order 84 | polynomial (FOP) will be fitted to the compressibility curve to 85 | calculate the maximum curvature point (MCP). If None, the MCP will 86 | be obtained using a cubic spline that passes through the data. The 87 | default is None. 88 | loglog : bool, optional 89 | Boolean to specify if the vertical effective stress will be 90 | transformed applying a logarithm twice to fit the FOT. The default 91 | is True. 92 | 93 | Returns 94 | ------- 95 | fig : matplotlib figure 96 | Figure with the development of the method and the results. 97 | 98 | """ 99 | 100 | def transform(x, reverse=False): 101 | if reverse: # Remove a logaritmic scale 102 | return 10**10**x if loglog else 10**x 103 | else: # Set a logaritmic scale 104 | return np.log10(np.log10(x)) if loglog else np.log10(x) 105 | 106 | # -- Indices to fit the Cubis Spline (CS) 107 | if range2fitCS is None: 108 | idxInitCS, idxEndCS = 1, len(self.data.cleaned["stress"]) + 1 109 | else: 110 | idxInitCS = self.data.findStressIdx( 111 | stress2find=range2fitCS[0], cleanedData=True 112 | ) 113 | idxEndCS = self.data.findStressIdx( 114 | stress2find=range2fitCS[1], cleanedData=True 115 | ) 116 | sigmaLog = np.log10(self.data.cleaned["stress"][idxInitCS:idxEndCS]) 117 | cs = CubicSpline( 118 | x=sigmaLog, y=self.data.cleaned["e"][idxInitCS:idxEndCS] 119 | ) 120 | sigmaCS = np.linspace(sigmaLog.iloc[0], sigmaLog.iloc[-1], 100) 121 | if range2fitFOP is None and mcp is None: # Using a cubic spline 122 | x4FOP = 10**sigmaCS 123 | # -- Curvature function k(x) = f''(x)/(1+(f'(x))²)³/² 124 | curvature = abs(cs(sigmaCS, 2)) / (1 + cs(sigmaCS, 1) ** 2) ** ( 125 | 3 / 2 126 | ) 127 | # maxCurvIdx = find_peaks( 128 | # curvature, distance=self.data.cleaned["stress"].max() 129 | # )[0][0] 130 | try: 131 | maxCurvIdx = find_max_not_at_ends(curvature) 132 | except Exception: 133 | print( 134 | "Maximun curvature not found matematicaly.", 135 | "Choosing the absolute maximum value.", 136 | sep="\n", 137 | ) 138 | maxCurvIdx = np.argmax(curvature) 139 | self.sigmaMC = 10 ** sigmaCS[maxCurvIdx] # Max. Curvature point 140 | self.eMC = cs(sigmaCS[maxCurvIdx]) # Void ratio at MC 141 | 142 | elif mcp is None: # Using a fourth order polynomial (FOP) 143 | # -- Indices to fit the FOP 144 | idxInitFOP = self.data.findStressIdx( 145 | stress2find=range2fitFOP[0], cleanedData=True 146 | ) 147 | idxEndFOP = self.data.findStressIdx( 148 | stress2find=range2fitFOP[1], cleanedData=True 149 | ) 150 | 151 | # -- fittig a polynomial to data without unloads 152 | sigmaFOP = self.data.cleaned["stress"][idxInitFOP:idxEndFOP] 153 | sigmaFOPlog = transform(sigmaFOP) 154 | eFOP = self.data.cleaned["e"][idxInitFOP:idxEndFOP] 155 | p0, p1, p2, p3, p4 = polyfit(sigmaFOPlog, eFOP, deg=4) 156 | r2FOP = r2_score( 157 | y_true=eFOP, y_pred=polyval(sigmaFOPlog, [p0, p1, p2, p3, p4]) 158 | ) 159 | x4FOP = np.linspace(sigmaFOP.iloc[0], sigmaFOP.iloc[-1], 1000) 160 | x4FOPlog = transform(x4FOP) 161 | y4FOP = polyval(x4FOPlog, [p0, p1, p2, p3, p4]) 162 | 163 | # -- Curvature function k(x) = f''(x)/(1+(f'(X))²)³/² 164 | firstDer = ( 165 | p1 166 | + 2 * p2 * x4FOPlog 167 | + 3 * p3 * x4FOPlog**2 168 | + 4 * p4 * x4FOPlog**3 169 | ) 170 | secondDer = 2 * p2 + 6 * p3 * x4FOPlog + 12 * p4 * x4FOPlog**2 171 | curvature = abs(secondDer) / (1 + firstDer**2) ** 1.5 172 | # maxCurvIdx = find_peaks( 173 | # curvature, distance=self.data.cleaned["stress"].max() 174 | # )[0][0] 175 | try: 176 | maxCurvIdx = find_max_not_at_ends(curvature) 177 | except Exception: 178 | print( 179 | "Maximun curvature not found matematicaly.", 180 | "Choosing the absolute maximum value.", 181 | sep="\n", 182 | ) 183 | maxCurvIdx = np.argmax(curvature) 184 | self.sigmaMC = transform(x4FOPlog[maxCurvIdx], True) # Max. Curv. 185 | self.eMC = y4FOP[maxCurvIdx] # Void ratio at max. curvature 186 | 187 | if mcp is not None: 188 | self.sigmaMC = mcp # Max. Curvature point 189 | self.eMC = cs(np.log10(self.sigmaMC)) # Void ratio at MC 190 | 191 | # -- Bisector line 192 | slopeMP = cs(np.log10(self.sigmaMC), nu=1) # Slope at MC 193 | y1, x1 = self.eMC, np.log10(self.sigmaMC) 194 | x2 = np.log10( 195 | np.linspace(self.sigmaMC, self.data.cleaned["stress"].iloc[-1], 50) 196 | ) 197 | y2 = slopeMP * (x2 - x1) + y1 198 | slopeBisect = np.tan( 199 | 0.5 * np.arctan(slopeMP) 200 | ) # slope of bisector line 201 | y2bis = slopeBisect * (x2 - x1) + y1 202 | 203 | # -- Preconsolidation pressure 204 | self.sigmaP = 10 ** ( 205 | (y1 - slopeBisect * x1 - self.data.idxCcInt) 206 | / (-self.data.idxCc - slopeBisect) 207 | ) 208 | self.eSigmaP = slopeBisect * (np.log10(self.sigmaP) - x1) + y1 209 | self.ocr = self.sigmaP / self.data.sigmaV 210 | 211 | # -- plotting 212 | fig = plt.figure(figsize=figsize) 213 | ax1 = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 214 | l1 = ax1.plot( 215 | self.data.raw["stress"][1:], 216 | self.data.raw["e"][1:], 217 | ls=(0, (1, 1)), 218 | marker="o", 219 | lw=1.5, 220 | c="k", 221 | mfc="w", 222 | label="Compressibility curve", 223 | ) 224 | # Lines of the Casagrande's method 225 | ax1.plot( 226 | [self.sigmaMC, self.data.cleaned["stress"].iloc[-1]], 227 | [self.eMC, self.eMC], 228 | ls="--", 229 | lw=1.125, 230 | c="k", 231 | ) # hztl line 232 | ax1.plot(10**x2, y2, ls="--", lw=1.125, color="k") # tangent line 233 | ax1.plot( 234 | 10**x2, y2bis, ls="--", lw=1.125, color="k" 235 | ) # bisector line 236 | # Compression index (Cc) 237 | x4Cc = np.linspace(self.sigmaP, self.data.cleaned["stress"].iloc[-1]) 238 | y4Cc = -self.data.idxCc * np.log10(x4Cc) + self.data.idxCcInt 239 | l2 = ax1.plot( 240 | x4Cc, 241 | y4Cc, 242 | ls="-", 243 | lw=1.5, 244 | color=colors[1], 245 | label=str().join(["$C_\mathrm{c}=$", f"{self.data.idxCc:.3f}"]), 246 | ) 247 | allLayers = l1 + l2 248 | if self.data.fitCc: 249 | l3 = ax1.plot( 250 | self.data.cleaned["stress"].iloc[self.data.maskCc], 251 | self.data.cleaned["e"].iloc[self.data.maskCc], 252 | ls="", 253 | marker="x", 254 | color=colors[1], 255 | label=f"Data for linear fit\n(R$^2={self.data.r2Cc:.3f}$)", 256 | ) 257 | allLayers.insert(2, l3[0]) 258 | if mcp is None: # Curvature 259 | ax2 = ax1.twinx() # second y axis for curvature function 260 | l6 = ax2.plot( 261 | x4FOP, 262 | curvature, 263 | ls="--", 264 | c=colors[0], 265 | lw=1.125, 266 | mfc="w", 267 | label="Curvature", 268 | ) 269 | if range2fitCS is not None: # Cubic spline 270 | l4 = ax1.plot( 271 | 10**sigmaCS, 272 | cs(sigmaCS), 273 | ls="--", 274 | lw=1.125, 275 | color=colors[2], 276 | label="Cubic spline", 277 | ) 278 | l5 = ax1.plot( 279 | self.data.cleaned["stress"][idxInitCS:idxEndCS], 280 | self.data.cleaned["e"][idxInitCS:idxEndCS], 281 | ls="", 282 | marker="+", 283 | c=colors[2], 284 | label="Data for cubic spline", 285 | ) 286 | allLayers += l4 + l5 287 | # if range2fitFOP is None: # Curvature 288 | if range2fitFOP is not None: # FOP fit 289 | l4 = ax1.plot( 290 | x4FOP, 291 | y4FOP, 292 | ls="--", 293 | lw=1.125, 294 | color=colors[2], 295 | label="$4^\mathrm{th}$-order polynomial", 296 | ) 297 | l5 = ax1.plot( 298 | sigmaFOP, 299 | eFOP, 300 | ls="", 301 | marker="+", 302 | c=colors[2], 303 | label=f"Data for linear fit\n(R$^2={r2FOP:.3f}$)", 304 | ) 305 | allLayers += l4 + l5 306 | allLayers += l6 307 | # Other plots 308 | l7 = ax1.plot( 309 | self.data.sigmaV, 310 | self.data.eSigmaV, 311 | ls="", 312 | marker="|", 313 | c="r", 314 | ms=15, 315 | mfc="w", 316 | mew=1.5, 317 | label=str().join( 318 | [ 319 | "$\sigma^\prime_\mathrm{v_0}=$ ", 320 | f"{self.data.sigmaV:.0f} kPa", 321 | ] 322 | ), 323 | ) 324 | l8 = ax1.plot( 325 | self.sigmaMC, 326 | self.eMC, 327 | ls="", 328 | marker="^", 329 | c=colors[0], 330 | mfc="w", 331 | mew=1.5, 332 | ms=7, 333 | label="Max. curvature point", 334 | ) 335 | l9 = ax1.plot( 336 | self.sigmaP, 337 | self.eSigmaP, 338 | ls="", 339 | marker="o", 340 | mfc="w", 341 | c=colors[0], 342 | ms=7, 343 | mew=1.5, 344 | label=str().join( 345 | [ 346 | "$\sigma^\prime_\mathrm{p}=$ ", 347 | f"{self.sigmaP:.0f} kPa\n", 348 | f"OCR= {self.ocr:.1f}", 349 | ] 350 | ), 351 | ) 352 | allLayers += l7 + l8 + l9 353 | # Legend 354 | labs = [layer.get_label() for layer in allLayers] 355 | ax1.legend( 356 | allLayers, 357 | labs, 358 | bbox_to_anchor=(1.125, 0.5), 359 | loc=6, 360 | title="$\\bf{Casagrande\ method}$", 361 | ) 362 | # Other details 363 | ax1.spines["top"].set_visible(False) 364 | ax1.spines["right"].set_visible(False) 365 | ax1.set( 366 | xscale="log", 367 | ylabel="Void ratio, $e$", 368 | xlabel=str().join( 369 | [ 370 | "Effective vertical stress, ", 371 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 372 | ] 373 | ), 374 | ) 375 | ax1.xaxis.set_major_formatter(mtick.ScalarFormatter()) 376 | ax1.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 377 | ax1.grid(False) 378 | if mcp is None: # Curvature 379 | ax2.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 380 | ax2.set(ylabel="Curvature, $k$") 381 | ax2.spines["top"].set_visible(False) 382 | return fig 383 | 384 | 385 | def find_max_not_at_ends(vector): 386 | """ 387 | Find the index of the maximum value of a vector that is not at the ends. 388 | 389 | Parameters 390 | ---------- 391 | vector : array_like 392 | Vector to find the maximum value. 393 | 394 | Returns 395 | ------- 396 | max_index : int 397 | Index of the maximum value of the vector that is not at the ends. 398 | 399 | """ 400 | 401 | while True: 402 | # Exclude the first and last element 403 | vector = vector[1:-1] 404 | # Find the index of the maximum value 405 | max_index = np.argmax(vector) 406 | # If the maximum value is not at the ends of the new vector, break the loop 407 | if max_index not in [0, len(vector) - 1]: 408 | break 409 | return max_index + 1 410 | 411 | 412 | # %% 413 | """ 414 | 2-Clause BSD License. 415 | 416 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 417 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 418 | UNIVERSIDAD NACIONAL DE COLOMBIA. 419 | 420 | Redistribution and use in source and binary forms, with or without 421 | modification, are permitted provided that the following conditions are met: 422 | 423 | 1. Redistributions of source code must retain the above copyright notice, this 424 | list of conditions and the following disclaimer. 425 | 426 | 2. Redistributions in binary form must reproduce the above copyright notice, 427 | this list of conditions and the following disclaimer in the documentation 428 | and/or other materials provided with the distribution. 429 | 430 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 431 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 432 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 433 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 434 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 435 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 436 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 437 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 438 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 439 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 440 | """ 441 | -------------------------------------------------------------------------------- /pysigmap/data.py: -------------------------------------------------------------------------------- 1 | """``data.py`` module. 2 | 3 | Contains the class and its methods for loading and managing the data of the 4 | compressibility curve. 5 | 6 | """ 7 | 8 | # -- Required modules 9 | import numpy as np 10 | from numpy.polynomial.polynomial import polyfit, polyval 11 | from scipy.interpolate import CubicSpline, interp1d 12 | import matplotlib as mpl 13 | import matplotlib.pyplot as plt 14 | import matplotlib.ticker as mtick 15 | import pandas as pd 16 | from mstools.mstools import r2_score 17 | 18 | from pysigmap import figsize, colors 19 | 20 | 21 | # plt.style.use("default") 22 | mpl.rcParams.update( 23 | { 24 | "text.usetex": False, # Use mathtext, not LaTeX 25 | "font.family": "serif", # Use the Computer modern font 26 | "font.serif": "cmr10", 27 | "mathtext.fontset": "cm", 28 | "axes.formatter.use_mathtext": True, 29 | "axes.unicode_minus": False, 30 | } 31 | ) 32 | 33 | 34 | class Data: 35 | """``Data`` class. 36 | 37 | When the object is instanced, the compression and recompression indices 38 | are automatically calculated with the default inputs of the methods. 39 | See the documentation of each method for more information regarding the 40 | default inputs. 41 | 42 | Attributes 43 | ---------- 44 | rawData : pandas DataFrame 45 | Data from the incremental loading oedometer test. It includes three 46 | series with the following and strict order: effective vertical stress, 47 | axial strain and void ratio. The first row must include the on-table 48 | void ratio of the specimen. 49 | sigmaV : float 50 | In situ effective vertical stress of the specimen. 51 | strainPercent : bool, optional 52 | Boolean to specify if the axial strain of the row data is in percent or 53 | not. The default is True. 54 | reloading : bool, optional 55 | Boolean to specify if the test included a reloading stage. The default 56 | is True. 57 | secondUnloading : bool, optional 58 | Boolean to specify if the test included two reloading stages. The 59 | default is True. 60 | use_this_UR_stage : int, optional 61 | Index to specify which unloading-reloading stage will be used to 62 | compute the recompression index when the test data includes more than 63 | one stage. Index notation as Python's standard (first stage → 0). The 64 | default is 0. 65 | 66 | Examples 67 | -------- 68 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 69 | >>> 'data4testing/main/pysigmap/testData.csv']) 70 | >>> urlXLSX = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 71 | >>> 'data4testing/main/pysigmap/testData.xlsx']) 72 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 73 | >>> # or 74 | >>> data = Data(pd.read_excel(urlXLSX), sigmaV=75) 75 | >>> data.plot() 76 | >>> data.idxCc, data.idxCr 77 | (0.23829647651319996, 0.04873212611656529) 78 | >>> data.compressionIdx(range2fitCc=(1000, 8000)) 79 | >>> data.plot() 80 | >>> data.idxCc 81 | 0.22754962258271252 82 | >>> data.recompressionIdx(opt=2) 83 | >>> data.plot() 84 | >>> data.idxCr 85 | 0.049481704199255676 86 | >>> data.recompressionIdx(opt=3) 87 | >>> data.plot() 88 | >>> data.idxCr 89 | 0.05398842524225007 90 | """ 91 | 92 | def __init__( 93 | self, 94 | rawData, 95 | sigmaV, 96 | strainPercent=True, 97 | reloading=True, 98 | secondUnloading=True, 99 | use_this_UR_stage=0, 100 | ): 101 | """Initialize the class.""" 102 | self.raw = rawData 103 | self.sigmaV = sigmaV 104 | self.strainPercent = strainPercent 105 | self.reloading = reloading 106 | self.secondUnloading = secondUnloading 107 | self.use_this_UR_stage = use_this_UR_stage 108 | self.preprocessing() 109 | self.getBreakIndices() 110 | self.clean() 111 | self.idxCc = None 112 | self.idxCr = None 113 | # self.compressionIdx() 114 | # self.recompressionIdx() 115 | return 116 | 117 | def preprocessing(self): 118 | """ 119 | Rename the series names and create the on-table void ratio attribute. 120 | 121 | Returns 122 | ------- 123 | None. 124 | """ 125 | # Standardizing series names 126 | self.raw.columns = ["stress", "strain", "e"] 127 | # Removing percentage format to strain values 128 | if self.strainPercent: 129 | self.raw["strain"] = self.raw["strain"].divide(100) 130 | # On-table (initial) void ratio 131 | self.e_0 = self.raw["e"].iloc[0] 132 | return 133 | 134 | def getBreakIndices(self): 135 | """ 136 | Find the break indices of the compressibility curve. 137 | 138 | The break indices are the following: 139 | 140 | - brkIdx1: Start of the first unloading stage 141 | - brkIdx2: End of the first unloading stage 142 | - brkIdx3: Point on the compression range after the first reloading 143 | - brkIdx4: Index of the last point on the compression range 144 | 145 | Returns 146 | ------- 147 | None. 148 | """ 149 | idx_unloading_init = [ # brkIdx1: Start of the first unloading 150 | i 151 | for i in self.raw.index[1:-1] 152 | if ( 153 | self.raw["stress"][i] > self.raw["stress"][i - 1] 154 | and self.raw["stress"][i] > self.raw["stress"][i + 1] 155 | ) 156 | ] 157 | if self.secondUnloading: 158 | idx_unloading_init.pop(-1) 159 | self.idx_unloading_init = idx_unloading_init 160 | brkIdx1 = idx_unloading_init[self.use_this_UR_stage] 161 | 162 | if self.reloading: # Cases 2-3: Reloading and second unloading 163 | idx_reloading_init = [ # brkIdx2: End of the first unloading 164 | i 165 | for i in self.raw.index[1:-1] 166 | if ( 167 | self.raw["stress"][i] < self.raw["stress"][i - 1] 168 | and self.raw["stress"][i] < self.raw["stress"][i + 1] 169 | ) 170 | ] 171 | brkIdx2 = idx_reloading_init[self.use_this_UR_stage] 172 | self.idx_reloading_init = idx_reloading_init 173 | 174 | # brkIdx3: Points on the NCL after the each reloading 175 | idx_reloading_ncl = [ 176 | self.raw.query(f"stress == stress[{i}]").index[1] 177 | for i in idx_unloading_init 178 | ] 179 | brkIdx3 = idx_reloading_ncl[self.use_this_UR_stage] 180 | self.idx_reloading_ncl = idx_reloading_ncl 181 | else: # Case 1: No reloading - No second unloading 182 | brkIdx2 = self.raw.index[-1] # last point of the unluading 183 | brkIdx3 = None 184 | # brkIdx4: index of the last point on the NCL 185 | brkIdx4 = self.raw.query("stress == stress.max()").index[0] 186 | 187 | self.brkIdx1 = brkIdx1 188 | self.brkIdx2 = brkIdx2 189 | self.brkIdx3 = brkIdx3 190 | self.brkIdx4 = brkIdx4 191 | return 192 | 193 | def clean(self): 194 | """ 195 | Clean the raw data removing the unloading and reloading stages. 196 | 197 | Returns 198 | ------- 199 | None. 200 | """ 201 | if self.reloading: 202 | idx_init = [-1] + self.idx_reloading_ncl 203 | idx_end = self.idx_unloading_init + [self.brkIdx4] 204 | self.cleaned = pd.concat( 205 | [ 206 | self.raw[idx_init[i] + 1 : idx_end[i] + 1] 207 | for i in range(len(idx_init)) 208 | ] 209 | ) 210 | else: 211 | self.cleaned = self.raw[: self.brkIdx1 + 1] 212 | self.cleaned.reset_index(drop=True, inplace=True) 213 | sigmaLog = np.log10(self.cleaned["stress"][1:]) 214 | # cs = CubicSpline(x=sigmaLog, y=self.cleaned["e"][1:]) 215 | # self.eSigmaV = float(cs(np.log10(self.sigmaV))) 216 | interpolator = interp1d(x=sigmaLog, y=self.cleaned["e"][1:]) 217 | self.eSigmaV = interpolator(np.log10(self.sigmaV)) 218 | return 219 | 220 | def findStressIdx(self, stress2find, cleanedData=True): 221 | """ 222 | Return the ceiling index of a specified stress. 223 | 224 | Parameters 225 | ---------- 226 | stress2find : float 227 | Stress whose ceiling index is wanted. 228 | cleanedData : bool, optional 229 | Boolean to indicate if the index must be find in the row or cleaned 230 | data. The default is True. 231 | 232 | Returns 233 | ------- 234 | idx : int 235 | The ceiling index of the stress input. 236 | """ 237 | if stress2find == 0: 238 | return 1 239 | elif stress2find > self.raw["stress"].max(): 240 | return None 241 | else: 242 | data4finding = self.cleaned if cleanedData else self.raw 243 | return data4finding.query(f"stress >= {stress2find}").index[0] 244 | 245 | def compressionIdx(self, range2fitCc=None): 246 | """ 247 | Calculate the compression index (Cc). 248 | 249 | Parameters 250 | ---------- 251 | range2fitCc : list, tuple or array (length=2), optional 252 | Initial and final pressures between which the first-order 253 | polynomial will be fit to the data on the compression range. If 254 | None, the Cc index will be calculated as the 255 | steepest slope of the cubic spline that passes through the data. 256 | The default is None. 257 | 258 | Returns 259 | ------- 260 | None. 261 | """ 262 | self.range2fitCc = range2fitCc 263 | if range2fitCc is None: # Using a cubic spline 264 | sigmaLog = np.log10(self.cleaned["stress"][1:]) 265 | cs = CubicSpline(x=sigmaLog, y=self.cleaned["e"][1:]) 266 | sigmaCS = np.linspace(sigmaLog.iloc[0], sigmaLog.iloc[-1], 500) 267 | steepestSlopeIdx = np.argmin(cs(sigmaCS, 1)) 268 | idxCc = cs(sigmaCS, 1)[steepestSlopeIdx] 269 | idxCcInt = ( 270 | cs(sigmaCS[steepestSlopeIdx]) 271 | - idxCc * sigmaCS[steepestSlopeIdx] 272 | ) 273 | self.fitCc = False 274 | else: # Using a linear fit of a first-order polynomial 275 | idxInitCc = self.findStressIdx( 276 | stress2find=range2fitCc[0], cleanedData=True 277 | ) 278 | idxEndCc = self.findStressIdx( 279 | stress2find=range2fitCc[1], cleanedData=True 280 | ) 281 | maskCc = np.full(len(self.cleaned), False) 282 | maskCc[idxInitCc:idxEndCc] = True 283 | self.maskCc = maskCc 284 | # -- Linear regresion 285 | sigmaCc = self.cleaned["stress"].iloc[maskCc] 286 | sigmaCclog = np.log10(sigmaCc) 287 | eCc = self.cleaned["e"].iloc[maskCc] 288 | idxCcInt, idxCc = polyfit(sigmaCclog, eCc, deg=1) 289 | r2Cc = r2_score( 290 | y_true=eCc, y_pred=polyval(sigmaCclog, [idxCcInt, idxCc]) 291 | ) 292 | self.r2Cc = r2Cc 293 | self.fitCc = True 294 | self.idxCc = abs(idxCc) 295 | self.idxCcInt = abs(idxCcInt) 296 | return 297 | 298 | def recompressionIdx(self, opt=1): 299 | """ 300 | Calculate the recompression index (Cr). 301 | 302 | Parameters 303 | ---------- 304 | opt : TYPE, optional 305 | Integer value to indicate which method will be used. Using only 306 | two points, the start and end of the unluading stage (opt=1); using 307 | all the points of the first unloading stage (opt=2); using the 308 | points of the first unloading and reloading stages (opt=3). The 309 | default is 1. 310 | 311 | Returns 312 | ------- 313 | None. 314 | """ 315 | maskCr = np.full(len(self.raw), False) 316 | if opt == 1: 317 | maskCr[self.brkIdx1] = True 318 | maskCr[self.brkIdx2] = True 319 | elif opt == 2: 320 | maskCr[self.brkIdx1 : self.brkIdx2 + 1] = True 321 | elif opt == 3: 322 | maskCr[self.brkIdx1 : self.brkIdx3 + 1] = True 323 | # -- Linear regresion 324 | sigmaCr = self.raw["stress"].iloc[maskCr] 325 | sigmaCrlog = np.log10(sigmaCr) 326 | eCr = self.raw["e"].iloc[maskCr] 327 | idxCrInt, idxCr = polyfit(sigmaCrlog, eCr, deg=1) 328 | r2Cr = r2_score( 329 | y_true=eCr, y_pred=polyval(sigmaCrlog, [idxCrInt, idxCr]) 330 | ) 331 | self.maskCr = maskCr 332 | self.r2Cr = r2Cr 333 | self.idxCr = abs(idxCr) 334 | self.idxCrInt = idxCrInt 335 | return 336 | 337 | def plot(self): 338 | """ 339 | Plot the compressibility curve, Cc and Cr indices. 340 | 341 | Returns 342 | ------- 343 | fig : matplotlib.figure.Figure 344 | Figure that includes the compressibility curve, Cc, Cr and the in 345 | situ effective vertical stress of the specimen. 346 | """ 347 | # -- plotting 348 | fig = plt.figure(figsize=figsize) 349 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 350 | ax.plot( 351 | self.raw["stress"][1:], 352 | self.raw["e"][1:], 353 | ls=(0, (1, 1)), 354 | marker="o", 355 | lw=1.5, 356 | c="k", 357 | mfc="w", 358 | label="Experimental data", 359 | ) 360 | ax.plot( 361 | self.sigmaV, 362 | self.eSigmaV, 363 | ls="", 364 | marker="|", 365 | c="r", 366 | ms=15, 367 | mfc="w", 368 | mew=1.5, 369 | label=str().join( 370 | ["$\sigma^\prime_\mathrm{v_0}=$ ", f"{self.sigmaV:.0f} kPa"] 371 | ), 372 | ) 373 | # Compression index 374 | if self.idxCc is not None: 375 | x4Cc = np.linspace( 376 | self.cleaned["stress"].iloc[-2], 377 | self.cleaned["stress"].iloc[-1], 378 | ) 379 | y4Cc = -self.idxCc * np.log10(x4Cc) + self.idxCcInt 380 | ax.axline( 381 | (x4Cc[0], y4Cc[0]), 382 | (x4Cc[-1], y4Cc[-1]), 383 | ls="-", 384 | lw=1.125, 385 | color=colors[1], 386 | label=str().join(["$C_\mathrm{c}=$", f"{self.idxCc:.3f}"]), 387 | ) 388 | if self.fitCc: 389 | ax.plot( 390 | self.cleaned["stress"].iloc[self.maskCc], 391 | self.cleaned["e"].iloc[self.maskCc], 392 | ls="", 393 | marker="x", 394 | color=colors[1], 395 | label=f"Data for linear fit\n(R$^2={self.r2Cc:.3f}$)", 396 | ) 397 | # Recompression index 398 | if self.idxCr is not None: 399 | x4Cr = np.linspace( 400 | self.raw["stress"].iloc[self.maskCr].min(), 401 | self.raw["stress"].iloc[self.maskCr].max(), 402 | ) 403 | y4Cr = -self.idxCr * np.log10(x4Cr) + self.idxCrInt 404 | ax.plot( 405 | x4Cr, 406 | y4Cr, 407 | ls="-", 408 | lw=1.125, 409 | color=colors[2], 410 | label=str().join(["$C_\mathrm{r}=$", f"{self.idxCr:.3f}"]), 411 | ) 412 | ax.plot( 413 | self.raw["stress"].iloc[self.maskCr], 414 | self.raw["e"].iloc[self.maskCr], 415 | ls="", 416 | marker="+", 417 | color=colors[2], 418 | label=f"Data for linear fit\n(R$^2={self.r2Cr:.3f}$)", 419 | ) 420 | # other details 421 | ax.spines["top"].set_visible(False) 422 | ax.spines["right"].set_visible(False) 423 | ax.set( 424 | xscale="log", 425 | ylabel="Void ratio, $e$", 426 | xlabel=str().join( 427 | [ 428 | "Effective vertical stress, ", 429 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 430 | ] 431 | ), 432 | ) 433 | ax.xaxis.set_major_formatter(mtick.ScalarFormatter()) 434 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 435 | ax.grid(False) 436 | ax.legend( 437 | bbox_to_anchor=(1.125, 0.5), 438 | loc=6, 439 | title="$\\bf{Compressibility\ curve}$", 440 | ) 441 | return fig 442 | 443 | 444 | # %% 445 | """ 446 | 2-Clause BSD License. 447 | 448 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 449 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 450 | UNIVERSIDAD NACIONAL DE COLOMBIA. 451 | 452 | Redistribution and use in source and binary forms, with or without 453 | modification, are permitted provided that the following conditions are met: 454 | 455 | 1. Redistributions of source code must retain the above copyright notice, this 456 | list of conditions and the following disclaimer. 457 | 458 | 2. Redistributions in binary form must reproduce the above copyright notice, 459 | this list of conditions and the following disclaimer in the documentation 460 | and/or other materials provided with the distribution. 461 | 462 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 463 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 464 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 465 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 466 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 467 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 468 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 469 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 470 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 471 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 472 | """ 473 | -------------------------------------------------------------------------------- /pysigmap/energy.py: -------------------------------------------------------------------------------- 1 | """``energy.py`` module. 2 | 3 | Contains the classes and theis methods to determine the preconsolidation 4 | pressure from a compressibility curve via the strain energy methods proposed by 5 | [Becker et al. (1987)](https://doi.org/10.1139/t87-070), 6 | [Morin (1988)](https://doi.org/10.1139/t88-096) and 7 | [Wang & Frost (2004)](https://doi.org/10.1139/t04-013). 8 | 9 | """ 10 | 11 | # -- Required modules 12 | import numpy as np 13 | from numpy.polynomial.polynomial import polyfit, polyval 14 | from scipy.interpolate import CubicSpline 15 | import matplotlib as mpl 16 | import matplotlib.pyplot as plt 17 | from mstools.mstools import r2_score 18 | from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes 19 | from mpl_toolkits.axes_grid1.inset_locator import mark_inset 20 | import matplotlib.ticker as mtick 21 | 22 | from pysigmap import figsize, colors 23 | 24 | mpl.rcParams.update( 25 | { 26 | "text.usetex": False, # Use mathtext, not LaTeX 27 | "font.family": "serif", # Use the Computer modern font 28 | "font.serif": "cmr10", 29 | "mathtext.fontset": "cm", 30 | "axes.formatter.use_mathtext": True, 31 | "axes.unicode_minus": False, 32 | } 33 | ) 34 | 35 | 36 | class BeckerEtAl: 37 | """``BeckerEtAl`` class. 38 | 39 | When the object is instanced, the method ``getSigmaP()`` calculates the 40 | preconsolidation pressure by the method proposed by Becker et al. (1987) 41 | based on the parameters of the method. See the method documentation for 42 | more information. 43 | 44 | Attributes 45 | ---------- 46 | data : Object instanced from the ``Data`` class. 47 | Contains the data structure from the oedometer test. See the class 48 | documentation for more information. 49 | 50 | Examples 51 | -------- 52 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 53 | >>> 'data4testing/main/pysigmap/testData.csv']) 54 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 55 | >>> method = BeckerEtAl(data) 56 | >>> method.getSigmaP(range2fitRR=None, range2fitCR=None, zoom=4) 57 | >>> method.sigmaP, method.ocr 58 | (670.2104847956236, 8.936139797274983) 59 | >>> method.getSigmaP(range2fitRR=None, range2fitCR=[700, 10000], zoom=4) 60 | >>> method.sigmaP, method.ocr 61 | (500.09903645877176, 6.667987152783623) 62 | """ 63 | 64 | def __init__(self, data): 65 | """Initialize the class.""" 66 | self.data = data 67 | self.morinFormulation = False 68 | self.calculateWork() 69 | return 70 | 71 | def calculateWork(self): 72 | """ 73 | Calculate the work per unit volume. 74 | 75 | Returns 76 | ------- 77 | None. 78 | 79 | """ 80 | sigma = self.data.raw["stress"].array 81 | epsilon = self.data.raw["strain"].array 82 | deltaWork = ( 83 | 0.5 * (epsilon[1:] - epsilon[:-1]) * (sigma[1:] + sigma[:-1]) 84 | ) 85 | deltaWork = np.hstack((0, deltaWork)) 86 | if self.morinFormulation: # Work per unit volume of solids 87 | deltaWork /= 1 + self.data.e_0 88 | self.data.raw["deltaWork"] = deltaWork 89 | self.data.raw["work"] = np.cumsum(deltaWork) 90 | self.data.clean() # Data without unloads 91 | return 92 | 93 | def getSigmaP( 94 | self, 95 | range2fitRR=None, 96 | range2fitCR=None, 97 | zoom=3, 98 | morinFormulation=False, 99 | ): 100 | """ 101 | Return the value of the preconsolidation pressure. 102 | 103 | Parameters 104 | ---------- 105 | range2fitRR : list, tuple or array (length=2), optional 106 | Initial and final pressures between which the first order 107 | polynomial will be fitted to the data on the recompression range 108 | (RR) (range before the preconsolidation pressure). If None, the 109 | first order polynomial will be fitted from the first point of the 110 | curve to the point before the in situ effective vertical stress. 111 | The default is None. 112 | range2fitCR : list, tuple or array (length=2), optional 113 | Initial and final pressures between which the first order 114 | polynomial will be fitted to the data on the compression range (CR) 115 | (range above the preconsolidation pressure). If None, the CR will 116 | be automatically fitted to the same points used for calculating the 117 | compression index with the ``Data`` class only if it was calculated 118 | with a linear fit, otherwise, the steepest slope of the cubic 119 | spline that passes through the data will be used. The default is 120 | None. 121 | zoom : int, optional 122 | Value to magnify the view of the first points of the curve and the 123 | preconsolidation pressure in an inset window. The default is 3. 124 | morinFormulation : bool, optional 125 | Boolean to specify if the work per unit volume of solids (Morin 126 | formulation) must be used instead of the work per unit volume 127 | (Becker formulation). The default is False. 128 | 129 | Returns 130 | ------- 131 | fig : matplotlib figure 132 | Figure with the development of the method and the results. 133 | 134 | """ 135 | # -- Calculating work 136 | if morinFormulation: 137 | self.morinFormulation = morinFormulation 138 | self.calculateWork() # Calculate again with Morin Formulation 139 | 140 | # -- Preyield range or recompression range (RR) 141 | maskRR = np.full(len(self.data.cleaned), False) 142 | if range2fitRR is None: # Indices for fitting the RR line 143 | idxInitRR = 0 144 | idxEndRR = self.data.findStressIdx( 145 | stress2find=self.data.sigmaV, cleanedData=True 146 | ) 147 | else: 148 | idxInitRR = self.data.findStressIdx( 149 | stress2find=range2fitRR[0], cleanedData=True 150 | ) 151 | idxEndRR = self.data.findStressIdx( 152 | stress2find=range2fitRR[1], cleanedData=True 153 | ) 154 | maskRR[idxInitRR:idxEndRR] = True 155 | # Linear regresion 156 | sigmaRR = self.data.cleaned["stress"][maskRR] 157 | workRR = self.data.cleaned["work"][maskRR] 158 | p1_0, p1_1 = polyfit(sigmaRR, workRR, deg=1) 159 | r2RR = r2_score(y_true=workRR, y_pred=polyval(sigmaRR, [p1_0, p1_1])) 160 | xRR = np.linspace(0, self.data.cleaned["stress"].iloc[-3]) 161 | yRR = polyval(xRR, [p1_0, p1_1]) 162 | 163 | # -- Post yield range or compression range (CR) 164 | maskCR = np.full(len(self.data.cleaned), False) 165 | if range2fitCR is not None or self.data.fitCc: # Using a linear fit 166 | if range2fitCR is not None: 167 | idxInitCR = self.data.findStressIdx( 168 | stress2find=range2fitCR[0], cleanedData=True 169 | ) 170 | idxEndCR = self.data.findStressIdx( 171 | stress2find=range2fitCR[1], cleanedData=True 172 | ) 173 | maskCR[idxInitCR:idxEndCR] = True 174 | elif self.data.fitCc: 175 | maskCR = self.data.maskCc 176 | # Linear regresion 177 | sigmaCR = self.data.cleaned["stress"][maskCR] 178 | workCR = self.data.cleaned["work"][maskCR] 179 | lcrInt, lcrSlope = polyfit(sigmaCR, workCR, deg=1) 180 | r2CR = r2_score( 181 | y_true=workCR, y_pred=polyval(sigmaCR, [lcrInt, lcrSlope]) 182 | ) 183 | else: # Using the steepest point of a cubic spline 184 | sigma = self.data.cleaned["stress"] 185 | cs = CubicSpline(x=sigma, y=self.data.cleaned["work"]) 186 | sigmaCS = np.linspace(sigma.iloc[0], sigma.iloc[-1], 500) 187 | steepestSlopeIdx = np.argmax(cs(sigmaCS, 1)) 188 | lcrSlope = cs(sigmaCS, 1)[steepestSlopeIdx] 189 | lcrInt = ( 190 | cs(sigmaCS[steepestSlopeIdx]) 191 | - lcrSlope * sigmaCS[steepestSlopeIdx] 192 | ) 193 | xCR = np.linspace( 194 | -lcrInt / lcrSlope, self.data.cleaned["stress"].iloc[-1] 195 | ) 196 | yCR = polyval(xCR, [lcrInt, lcrSlope]) 197 | 198 | # -- Preconsolitadion pressure 199 | workSigmaV = polyval(self.data.sigmaV, [p1_0, p1_1]) 200 | self.sigmaP = (lcrInt - p1_0) / (p1_1 - lcrSlope) 201 | self.wSigmaP = polyval(self.sigmaP, [p1_0, p1_1]) 202 | self.ocr = self.sigmaP / self.data.sigmaV 203 | 204 | # -- Plot compresibility curve 205 | fig = plt.figure(figsize=figsize) 206 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 207 | ax.plot( 208 | self.data.raw["stress"], 209 | self.data.raw["work"], 210 | ls=(0, (1, 1)), 211 | marker="o", 212 | lw=1.5, 213 | c="k", 214 | mfc="w", 215 | label="Data", 216 | ) # all data 217 | # Recompression range 218 | ax.plot( 219 | xRR, 220 | yRR, 221 | ls="-", 222 | c=colors[2], 223 | lw=1.125, 224 | label="Recompression range", 225 | ) 226 | ax.plot( 227 | sigmaRR, 228 | workRR, 229 | ls="", 230 | marker="+", 231 | c=colors[2], 232 | label=f"Data for linear fit\n(R$^2={r2RR:.3f}$)", 233 | ) 234 | # Compression range 235 | ax.plot( 236 | xCR, yCR, ls="-", c=colors[1], lw=1.125, label="Compression range" 237 | ) 238 | if range2fitCR is not None or self.data.fitCc: 239 | ax.plot( 240 | sigmaCR, 241 | workCR, 242 | ls="", 243 | marker="x", 244 | c=colors[1], 245 | label=f"Data for linear fit\n(R$^2={r2CR:.3f}$)", 246 | ) 247 | # Other plots 248 | ax.plot( 249 | self.data.sigmaV, 250 | workSigmaV, 251 | ls="", 252 | marker="|", 253 | c="r", 254 | ms=15, 255 | mfc="w", 256 | mew=1.5, 257 | label=str().join( 258 | [ 259 | "$\sigma^\prime_\mathrm{v0}=$ ", 260 | f"{self.data.sigmaV:.0f} kPa", 261 | ] 262 | ), 263 | ) 264 | ax.plot( 265 | self.sigmaP, 266 | self.wSigmaP, 267 | ls="", 268 | c=colors[0], 269 | ms=7, 270 | mfc="w", 271 | marker="o", 272 | mew=1.5, 273 | label=str().join( 274 | [ 275 | "$\sigma^\prime_\mathrm{p}=$ ", 276 | f"{self.sigmaP:.0f} kPa\n", 277 | f"OCR= {self.ocr:.1f}", 278 | ] 279 | ), 280 | ) 281 | # Other details 282 | if morinFormulation: 283 | methodTitle = "$\\bf{Morin\ method}$" 284 | else: 285 | methodTitle = "$\\bf{Becker\ et\ al.\ method}$" 286 | ax.spines["top"].set_visible(False) 287 | ax.spines["right"].set_visible(False) 288 | ax.set( 289 | ylabel="Total work per unit volume, $W$ [kJ m$^{-3}$]", 290 | xlabel=str().join( 291 | [ 292 | "Effective vertical stress, ", 293 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 294 | ] 295 | ), 296 | ) 297 | ax.xaxis.set_minor_locator(mtick.AutoMinorLocator()) 298 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 299 | ax.grid(False) 300 | ax.legend(bbox_to_anchor=(1.125, 0.5), loc=6, title=methodTitle) 301 | 302 | # -- inset axes to zoom 303 | axins = zoomed_inset_axes(ax, zoom=zoom, loc=4) 304 | axins.plot( 305 | self.data.raw["stress"], 306 | self.data.raw["work"], 307 | ls=":", 308 | marker="o", 309 | lw=1.5, 310 | c="k", 311 | mfc="w", 312 | ) # all data 313 | axins.plot(xRR, yRR, ls="-", c=colors[2], lw=1.125) 314 | axins.plot(sigmaRR, workRR, ls="", marker="+", c=colors[2]) 315 | axins.plot(xCR, yCR, ls="-", c=colors[1], lw=1.125) 316 | if range2fitCR is not None or self.data.fitCc: 317 | axins.plot(sigmaCR, workCR, ls="", marker="x", c=colors[1]) 318 | axins.plot( 319 | self.data.sigmaV, 320 | workSigmaV, 321 | ls="", 322 | marker="|", 323 | c="r", 324 | ms=15, 325 | mfc="w", 326 | mew=1.5, 327 | ) 328 | axins.plot( 329 | self.sigmaP, 330 | self.wSigmaP, 331 | marker="o", 332 | c=colors[0], 333 | ms=7, 334 | mfc="w", 335 | mew=1.5, 336 | ) 337 | # axins.spines['bottom'].set_visible(False) 338 | # axins.spines['right'].set_visible(False) 339 | axins.grid(False) 340 | axins.set( 341 | xlim=(-0.05 * self.sigmaP, 1.25 * self.sigmaP), 342 | ylim=(-0.05 * self.wSigmaP, 2.05 * self.wSigmaP), 343 | xlabel="$\sigma^\prime_\mathrm{v}$ [kPa]", 344 | ylabel="W [kJ m$^{-3}$]", 345 | ) 346 | axins.xaxis.tick_top() 347 | axins.xaxis.set_label_position("top") 348 | axins.yaxis.tick_right() 349 | axins.yaxis.set_label_position("right") 350 | axins.xaxis.set_tick_params(labelsize="small") 351 | axins.yaxis.set_tick_params(labelsize="small") 352 | axins.xaxis.set_minor_locator(mtick.AutoMinorLocator()) 353 | axins.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 354 | # axins.set_yticks([]) 355 | mark_inset(ax, axins, loc1=2, loc2=3, fc="none", ec="0.5") # bbox 356 | return fig 357 | 358 | 359 | class WangAndFrost(BeckerEtAl): 360 | """``WangAndFrost`` class. 361 | 362 | When the object is instanced, the method ``getSigmaP()`` calculates the 363 | preconsolidation pressure by the method proposed by Wang & Frost (2004) 364 | based on the parameters of the method. See the method documentation for 365 | more information. 366 | 367 | Attributes 368 | ---------- 369 | data : Object instanced from the ``Data`` class. 370 | Contains the data structure from the consolidation test. See the class 371 | documentation for more information. 372 | 373 | Examples 374 | -------- 375 | >>> urlCSV = ''.join(['https://raw.githubusercontent.com/eamontoyaa/', 376 | >>> 'data4testing/main/pysigmap/testData.csv']) 377 | >>> data = Data(pd.read_csv(urlCSV), sigmaV=75) 378 | >>> method = WangAndFrost(data) 379 | >>> method.getSigmaP(range2fitCR=None) 380 | >>> method.sigmaP, method.ocr 381 | (930.59421331855, 12.407922844247334) 382 | >>> method.getSigmaP(range2fitCR=[700, 10000]) 383 | >>> method.sigmaP, method.ocr 384 | (718.9365936678746, 9.585821248904995) 385 | """ 386 | 387 | def __init__(self, data): 388 | """Initialize the class.""" 389 | # Heritage BeckerEtAl class 390 | BeckerEtAl.__init__(self, data) 391 | return 392 | 393 | def calculateDissipatedE(self, range2fitCR=None): 394 | """ 395 | Calculate the accumulative dissipated strain energy corrected (ADSEC). 396 | 397 | Obtain the correction value based on the linear regression on the ADSE 398 | line or the tangent line to the steepest point of the cubic spline that 399 | passes through the points. 400 | 401 | Parameters 402 | ---------- 403 | range2fitCR : list, tuple or array (length=2), optional 404 | Initial and final pressures between which the first order 405 | polynomial will be fitted to the data on the compression range (CR) 406 | (range above the preconsolidation pressure). If None, the CR will 407 | be automatically fitted to the same points used for calculating the 408 | compression index with the ``Data`` class only if it was calculated 409 | with a linear fitted, otherwise, the steepest slope of the cubic 410 | spline that passes through the data will be used. The default is 411 | None. 412 | 413 | Returns 414 | ------- 415 | None. 416 | 417 | """ 418 | # -- Incremental total strain energy (ITSE = deltaWork) 419 | self.data.raw["ITSE"] = self.data.raw["deltaWork"] 420 | # -- Accumulative total strain energy (ATSE = work) 421 | self.data.raw["ATSE"] = self.data.raw["work"] 422 | # -- Calculating the accumulative elastic strain energy (AESE) 423 | self.data.raw["AESE"] = ( 424 | self.data.raw["stress"] * self.data.idxCr / (1 + self.data.e_0) 425 | ) 426 | # -- Calculating the accumulative dissipated strain energy (ADSE) 427 | self.data.raw["ADSE"] = self.data.raw["ATSE"] - self.data.raw["AESE"] 428 | # -- Calculating value for correcting ADSE (OR: corrVal) 429 | self.data.clean() # Updating data without unloads 430 | if range2fitCR is not None or self.data.fitCc: 431 | s2fitTE = self.data.cleaned["stress"][self.maskCR] 432 | w2fitTE = self.data.cleaned["ATSE"][self.maskCR] 433 | self.corrVal, _ = polyfit(s2fitTE, w2fitTE, deg=1) 434 | else: 435 | cs = CubicSpline( 436 | x=self.data.cleaned["stress"], y=self.data.cleaned["ATSE"] 437 | ) 438 | sigmaCS = np.linspace(0, self.data.cleaned["stress"].iloc[-1], 500) 439 | steepestSlopeIdx = np.argmax(cs(sigmaCS, 1)) 440 | lcrSlope = cs(sigmaCS, 1)[steepestSlopeIdx] 441 | self.corrVal = ( 442 | cs(sigmaCS[steepestSlopeIdx]) 443 | - lcrSlope * sigmaCS[steepestSlopeIdx] 444 | ) 445 | # -- Calculating the accumulative total strain energy corrected (ATSEC) 446 | self.data.raw["ATSEC"] = self.data.raw["ATSE"] + abs(self.corrVal) 447 | # -- Accumulative dissipated strain energy corrected (ADSEC) 448 | self.data.raw["ADSEC"] = self.data.raw["ADSE"] + abs(self.corrVal) 449 | # -- Updating data without unloads 450 | self.data.clean() 451 | return 452 | 453 | def getSigmaP(self, range2fitCR=None): 454 | """ 455 | Return the value of the preconsolidation pressure. 456 | 457 | Parameters 458 | ---------- 459 | range2fitCR : list, tuple or array (length=2), optional 460 | Initial and final pressures between which the first order 461 | polynomial will be fitted to the data on the compression range (CR) 462 | (range above the preconsolidation pressure). If None, the CR will 463 | be automatically fitted to the same points used for calculating the 464 | compression index with the ``Data`` class only if it was calculated 465 | with a linear fit, otherwise, the steepest slope of the cubic 466 | spline that passes through the data will be used. The default is 467 | None. 468 | 469 | Returns 470 | ------- 471 | fig : matplotlib figure 472 | Figure with the development of the method and the results. 473 | 474 | """ 475 | # -- Post yield range or compression range (CR) 476 | self.maskCR = np.full(len(self.data.cleaned), False) 477 | if range2fitCR is not None or self.data.fitCc: # Using a linear fit 478 | if range2fitCR is not None: 479 | idxInitCR = self.data.findStressIdx( 480 | stress2find=range2fitCR[0], cleanedData=True 481 | ) 482 | idxEndCR = self.data.findStressIdx( 483 | stress2find=range2fitCR[1], cleanedData=True 484 | ) 485 | self.maskCR[idxInitCR:idxEndCR] = True 486 | elif self.data.fitCc: 487 | self.maskCR = self.data.maskCc 488 | # -- Calculatting the dissipated strain energy corrected (ADSEC) 489 | self.calculateDissipatedE(range2fitCR) 490 | # -- Linear regresion of points on post yield line 491 | sigmaCR = self.data.cleaned["stress"][self.maskCR] 492 | disspE = self.data.cleaned["ADSEC"][self.maskCR] 493 | lcrInt, lcrSlope = polyfit(sigmaCR, disspE, deg=1) 494 | r2CR = r2_score( 495 | y_true=disspE, y_pred=polyval(sigmaCR, [lcrInt, lcrSlope]) 496 | ) 497 | 498 | else: # Using the steepest point of a cubic spline 499 | # -- Calculatting the dissipated strain energy corrected (ADSEC) 500 | self.calculateDissipatedE(range2fitCR) 501 | sigma = self.data.cleaned["stress"] 502 | cs = CubicSpline(x=sigma, y=self.data.cleaned["ADSEC"]) 503 | sigmaCS = np.linspace(sigma.iloc[0], sigma.iloc[-1], 500) 504 | steepestSlopeIdx = np.argmax(cs(sigmaCS, 1)) 505 | lcrSlope = cs(sigmaCS, 1)[steepestSlopeIdx] 506 | lcrInt = ( 507 | cs(sigmaCS[steepestSlopeIdx]) 508 | - lcrSlope * sigmaCS[steepestSlopeIdx] 509 | ) 510 | 511 | # -- Preconsolitadion pressure 512 | self.sigmaP = (abs(self.corrVal) - lcrInt) / lcrSlope 513 | self.sseSigmaP = polyval(self.sigmaP, [lcrInt, lcrSlope]) 514 | self.ocr = self.sigmaP / self.data.sigmaV 515 | 516 | xCR = np.linspace(self.sigmaP, self.data.cleaned["stress"].iloc[-1]) 517 | yCR = polyval(xCR, [lcrInt, lcrSlope]) 518 | 519 | # -- Plot compresibility curve 520 | fig = plt.figure(figsize=figsize) 521 | ax = fig.add_axes([0.08, 0.12, 0.55, 0.85]) 522 | ax.plot( 523 | self.data.raw["stress"], 524 | self.data.raw["ATSEC"], 525 | ls=":", 526 | marker="v", 527 | lw=0.5, 528 | c="gray", 529 | mfc="w", 530 | label="Total energy", 531 | ) 532 | ax.plot( 533 | self.data.raw["stress"], 534 | self.data.raw["AESE"], 535 | ls=":", 536 | marker="s", 537 | lw=0.5, 538 | c="gray", 539 | mfc="w", 540 | label="Elastic energy", 541 | ) 542 | ax.plot( 543 | self.data.raw["stress"], 544 | self.data.raw["ADSEC"], 545 | ls=":", 546 | marker="o", 547 | lw=1.5, 548 | c="k", 549 | mfc="w", 550 | label="Dissipated energy", 551 | ) 552 | ax.hlines( 553 | y=abs(self.corrVal), 554 | xmin=0, 555 | xmax=self.sigmaP, 556 | lw=1.125, 557 | color=colors[2], 558 | ) 559 | # Compression range 560 | ax.plot( 561 | xCR, yCR, ls="-", c=colors[1], lw=1.125, label="Compression range" 562 | ) 563 | if range2fitCR is not None or self.data.fitCc: 564 | ax.plot( 565 | sigmaCR, 566 | disspE, 567 | ls="", 568 | marker="x", 569 | c=colors[1], 570 | label=f"Data for linear fit\n(R$^2={r2CR:.3f}$)", 571 | ) 572 | # Other plots 573 | ax.plot( 574 | self.data.sigmaV, 575 | -self.corrVal, 576 | ls="", 577 | marker="|", 578 | c="r", 579 | ms=15, 580 | mfc="w", 581 | mew=1.5, 582 | label=str().join( 583 | [ 584 | "$\sigma^\prime_\mathrm{v0}=$ ", 585 | f"{self.data.sigmaV:.0f} kPa", 586 | ] 587 | ), 588 | ) 589 | ax.plot( 590 | self.sigmaP, 591 | self.sseSigmaP, 592 | ls="", 593 | marker="o", 594 | c=colors[0], 595 | ms=7, 596 | mfc="w", 597 | mew=1.5, 598 | label=str().join( 599 | [ 600 | "$\sigma^\prime_\mathrm{p}=$ ", 601 | f"{self.sigmaP:.0f} kPa\n", 602 | f"OCR= {self.ocr:.1f}", 603 | ] 604 | ), 605 | ) 606 | # Other details 607 | ax.spines["top"].set_visible(False) 608 | ax.spines["right"].set_visible(False) 609 | ax.set( 610 | ylabel="Specific strain energy [kPa]", 611 | xlabel=str().join( 612 | [ 613 | "Effective vertical stress, ", 614 | "$\sigma^\prime_\mathrm{v}$ [kPa]", 615 | ] 616 | ), 617 | ) 618 | ax.xaxis.set_minor_locator(mtick.AutoMinorLocator()) 619 | ax.yaxis.set_minor_locator(mtick.AutoMinorLocator()) 620 | ax.grid(False) 621 | ax.legend( 622 | bbox_to_anchor=(1.125, 0.5), 623 | loc=6, 624 | title="$\\bf{Wang\ and\ Frost\ method}$", 625 | ) 626 | return fig 627 | 628 | 629 | # %% 630 | """ 631 | 2-Clause BSD License. 632 | 633 | Copyright 2020, EXNEYDER A. MONTOYA-ARAQUE, A. J. APARICIO-ORTUBE, 634 | DAVID G. ZAPATA-MEDINA, LUIS G. ARBOLEDA-MONSALVE AND 635 | UNIVERSIDAD NACIONAL DE COLOMBIA. 636 | 637 | Redistribution and use in source and binary forms, with or without 638 | modification, are permitted provided that the following conditions are met: 639 | 640 | 1. Redistributions of source code must retain the above copyright notice, this 641 | list of conditions and the following disclaimer. 642 | 643 | 2. Redistributions in binary form must reproduce the above copyright notice, 644 | this list of conditions and the following disclaimer in the documentation 645 | and/or other materials provided with the distribution. 646 | 647 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 648 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 649 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 650 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 651 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 652 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 653 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 654 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 655 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 656 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 657 | """ 658 | --------------------------------------------------------------------------------