├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.txt ├── README.md ├── environment-fromhistory.yml ├── environment.yml ├── images ├── Core2Relperm-logo.png └── logo.png └── python ├── SS_synthetic_data_imbibition_from-Excel.ipynb ├── SS_synthetic_data_imbibition_to-Excel.ipynb ├── USS_synthetic_data_drainage_from-Excel.ipynb ├── USS_synthetic_data_drainage_to-Excel.ipynb ├── USS_synthetic_data_imbibition_from-Excel.ipynb ├── USS_synthetic_data_imbibition_to-Excel.ipynb ├── benchmark_scores_Case1.ipynb ├── benchmark_scores_Case2.ipynb ├── benchmark_scores_Case3.ipynb ├── benchmark_scores_Case4.ipynb ├── example_Fig09_USS_dpw+dpo+noSwz.ipynb ├── example_Fig09_USS_dpw+dpo+noSwz.py ├── example_Fig17_USS_dpw+dpo+Swz_bumpfloods.ipynb ├── example_Fig17_USS_dpw+dpo+Swz_bumpfloods.py ├── expdataHISSSimbibition.xlsx ├── expdataHISUSSdrainage.xlsx ├── expdataHISUSSimbibition.xlsx ├── scallib001 ├── displacementmodel1D2P001.py ├── relpermlib001.py └── tests │ ├── README.md │ ├── conftest.py │ ├── test_power_eps1.py │ ├── test_relperm_Corey1.py │ ├── test_relperm_LET1.py │ ├── test_relperm_input.py │ ├── test_relpermlib1.py │ ├── test_relpermlib_cpubench1.py │ ├── test_simulation1.py │ └── test_solver_cpubench1.py └── scores_benchmark_data ├── Case_1_SCA_SCORES.csv ├── Case_2_SCA_SCORES.csv ├── Case_3_SCA_SCORES.csv ├── Case_4_SCA_SCORES.csv ├── __init__.py └── read_scores_data.py /.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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | **This code of conduct outlines expectations for participation in Shell-managed open source communities, as well as steps for reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all. People violating this code of conduct may be banned from the community.** 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | This Code of Conduct also applies outside the project spaces when there is a 60 | reasonable belief that an individual's behavior may have a negative impact on 61 | the project or its community. 62 | 63 | ## Enforcement 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 66 | reported to the community leaders responsible for enforcement at 67 | [INSERT CONTACT METHOD]. 68 | All complaints will be reviewed and investigated promptly and fairly. 69 | 70 | All community leaders are obligated to respect the privacy and security of the 71 | reporter of any incident. 72 | 73 | ## Enforcement Guidelines 74 | 75 | Community leaders will follow these Community Impact Guidelines in determining 76 | the consequences for any action they deem in violation of this Code of Conduct: 77 | 78 | ### 1. Correction 79 | 80 | **Community Impact**: Use of inappropriate language or other behavior deemed 81 | unprofessional or unwelcome in the community. 82 | 83 | **Consequence**: A private, written warning from community leaders, providing 84 | clarity around the nature of the violation and an explanation of why the 85 | behavior was inappropriate. A public apology may be requested. 86 | 87 | ### 2. Warning 88 | 89 | **Community Impact**: A violation through a single incident or series of 90 | actions. 91 | 92 | **Consequence**: A warning with consequences for continued behavior. No 93 | interaction with the people involved, including unsolicited interaction with 94 | those enforcing the Code of Conduct, for a specified period of time. This 95 | includes avoiding interactions in community spaces as well as external channels 96 | like social media. Violating these terms may lead to a temporary or permanent 97 | ban. 98 | 99 | ### 3. Temporary Ban 100 | 101 | **Community Impact**: A serious violation of community standards, including 102 | sustained inappropriate behavior. 103 | 104 | **Consequence**: A temporary ban from any sort of interaction or public 105 | communication with the community for a specified period of time. No public or 106 | private interaction with the people involved, including unsolicited interaction 107 | with those enforcing the Code of Conduct, is allowed during this period. 108 | Violating these terms may lead to a permanent ban. 109 | 110 | ### 4. Permanent Ban 111 | 112 | **Community Impact**: Demonstrating a pattern of violation of community 113 | standards, including sustained inappropriate behavior, harassment of an 114 | individual, or aggression toward or disparagement of classes of individuals. 115 | 116 | **Consequence**: A permanent ban from any sort of public interaction within the 117 | community. 118 | 119 | ## Attribution 120 | 121 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 122 | version 2.1, available at 123 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 124 | 125 | Community Impact Guidelines were inspired by 126 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 127 | 128 | Expanding scope to include external impact on community health inspired by 129 | [Facebook's Open Source Code of Conduct](https://opensource.facebook.com/code-of-conduct) 130 | and [Mozilla's Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/). 131 | 132 | For answers to common questions about this code of conduct, see the FAQ at 133 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 134 | [https://www.contributor-covenant.org/translations][translations]. 135 | 136 | [homepage]: https://www.contributor-covenant.org 137 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 138 | [Mozilla CoC]: https://github.com/mozilla/diversity 139 | [FAQ]: https://www.contributor-covenant.org/faq 140 | [translations]: https://www.contributor-covenant.org/translations 141 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sede-open 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 - present Shell Global Solutions International B.V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | Table of Contents 6 |
    7 |
  1. 8 | About The Project 9 |
  2. 10 |
  3. 11 | Getting Started 12 | 16 |
  4. 17 |
  5. Usage
  6. 18 |
  7. Roadmap
  8. 19 |
  9. Contributing
  10. 20 |
  11. License
  12. 21 |
  13. Contact
  14. 22 |
  15. Acknowledgments
  16. 23 |
  17. How to Cite
  18. 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | Logo 33 | 34 | 35 |

Core2Relperm

36 | 37 |

38 | A Python library for interpreting core flooding experiments 39 |
40 | Explore the docs » 41 |
42 |
43 | View Demo 44 | · 45 | Report Bug 46 | · 47 | Request Feature 48 |

49 |
50 | 51 | 52 | 53 | ## About The Project 54 | 55 | [![Product Name Screen Shot][product-screenshot]](https://github.com/sede-open/Core2Relperm) 56 | 57 | For modelling studies of underground storage of carbon dioxide and hydrogen, transport in the vadoze zone, contaminant hydrology as well as hydrocarbon recovery, it is important to have a consistent set of relative permeability and capillary pressure-saturation functions as inputs for numerical reservoir models in order to assess risks and uncertainties and provide forward-models for different scenarios. 58 | Such relative permeability and capillary-pressure saturations functions are typically obtained in Special Core Analysis (SCAL) where core flooding experiments are a central element (see also The Society of Core Analysts). Interpreation of such core flooding experiments by analytical approximations has several disadvantages and instead, interpretation by inverse modelling is the preferred approach. 59 | This project has been created to provide a standalone Python tool for the interpretation of such core flooding experiments. 60 | It contains 61 | 65 | The inverse modelling framework is in its default version a least-squares fit using the Levenberg-Marquardt algorithm. It essentially performs a least-squares fit of the numerical solution of a set of partial differential equations (which are numerically solved by the flow solver) to numerical data. The Jacobian is automatically computed numerically in the background by the lmfit package. 66 | The flow solver is accelerated with the numba just-in-time compiler which makes the flow solver code run in just about 50 ms. 67 | For a few tens of iterations required for a typical inverse modelling with least-squares fit, the code runs just in a few seconds. One can also change an option in the lmfit package (only a single line) to using the emcee Markov chain Monte Carlo (MCMC) package. About 10,000-20,000 iterations will run in a few hours in single-threaded mode. The advantage of using the MCMC approach is that one can address problems non-uniqueness and non-Gaussian errors. 68 | 69 | Flow simulator code and inverse modelling framework are research code. The 1D flow code has been validated against benchmarks developed by Jos Maas and respective benchmark examples are included as examples. The inverse modelling framework has been validated in a series of publications 70 | 71 | 1. S. Berg, E. Unsal, H. Dijk, Non-Uniqueness and Uncertainty Quantification of Relative Permeability Measurements by Inverse Modelling, Computers and Geotechnics 132, 103964, 2021. 72 | 73 | 2. S. Berg, E. Unsal, H. Dijk, Sensitivity and uncertainty analysis for parameterization of multi phase flow models, Transport in Porous Media 140(1), 27-57, 2021. 74 | 75 | 3. S. Berg, H. Dijk, E. Unsal, R. Hofmann, B. Zhao, V. Ahuja, Simultaneous Determination of Relative Permeability and Capillary Pressure from an Unsteady-State Core Flooding Experiment ? Computers and Geotechnics 168, 106091, 2024. 76 | 77 | 4. R. Lenormand, K. Lorentzen, J. G. Maas and D. Ruth 78 | COMPARISON OF FOUR NUMERICAL SIMULATORS FOR SCAL EXPERIMENTS 79 | SCA2016-006 80 | 81 | 82 |

(back to top)

83 | 84 | 85 |

(back to top)

86 | 87 | 88 | 89 | ## Getting Started 90 | 91 | Read the paper to get some background info. Then install your favorite Python distribution of you don't already have one (we used Anaconda), 92 | install required libraries, download the code and run the examples. 93 | 94 | 95 | ### Dependencies 96 | 97 | The code and examples can be run from most modern Python distributions such as Anaconda. You may want to choose a distribution that has `matplotlib`, `numpy` and other standard packages pre-installed. There are a few extra libraries to install: 98 | 99 | * pandas (using internally pandas data frames, but also to import/export data) 100 | * lmfit (the engine for the least squares fits) 101 | * emcee (Markov chain Monte Carlo sampler, optional) 102 | * numba (Just In Time compiler) 103 | * seaborn (for statistical data visualization) 104 | * openpyxl (for reading/writing Excel files, which are used to store experimental data) 105 | 106 | ### Installation 107 | 108 | Quick installation by replicating the environment in Anaconda: 109 | 110 | 1. Clone the repo 111 | ```sh 112 | git clone https://github.com/sede-open/core2relperm.git 113 | ``` 114 | 2. Configure conda 115 | ```sh 116 | conda update conda 117 | conda config --set ssl_verify false 118 | ``` 119 | 3. Replicate environment using either of the following commands: 120 | ```sh 121 | conda env create -f environment.yml 122 | ``` 123 | 4. Activate the environment 124 | ```sh 125 | conda activate relperm 126 | ``` 127 | 128 | The environment.yml file does not contain specific versions. We have noticed that on some systems this can create problems. For that reason, we created a second environment file environment-fromhistory.yml which contains specific versions of packages that has been tested on a wider range of systems. To install, use the same command as in (3) but with environment-fromhistory.yml 129 | 130 | Alternatively, if you face issues with above mentioned quick installation, you can create the environment and install the Python packages manually as shown below: 131 | 132 | 1. Create new environment and install required Python libraries 133 | ```sh 134 | conda create -n relperm numpy matplotlib numba scipy seaborn pandas lmfit emcee openpyxl 135 | ``` 136 | 2. For rendering in VSCode install the ipykernel package 137 | ```sh 138 | conda install ipykernel 139 | ``` 140 | 141 | 142 | 143 | 144 | ## Usage 145 | 146 | ### Solver Benchmarks 147 | 148 | We included 4 SCAL benchmarks from https://www.jgmaas.com 149 | 150 | ``` 151 | benchmark_scores_Case1.ipynb 152 | benchmark_scores_Case2.ipynb 153 | benchmark_scores_Case3.ipynb 154 | benchmark_scores_Case4.ipynb 155 | ``` 156 | 157 | 158 | that are benchmarking the 2-phase 1D flow solver defined in 159 | R. Lenormand, K. Lorentzen, J. G. Maas and D. Ruth, COMPARISON OF FOUR NUMERICAL SIMULATORS FOR SCAL EXPERIMENTS, SCA2016-006 160 | 161 | 162 | 163 | 164 | ### Inverse Modelling Examples from latest Computers & GeoTechnics Paper 165 | 166 | We include 2 examples from the paper S. Berg, H. Dijk, E. Unsal, R. Hofmann, B. Zhao, V. Ahuja, Simultaneous Determination of Relative Permeability and Capillary Pressure from an Unsteady-State Core Flooding Experiment ? Computers and Geotechnics 168, 106091, 2024. 167 | 168 | * Fig. 09 169 | ```sh 170 | example_Fig09_USS_dpw+dpo+noSwz.py 171 | ``` 172 | * Fig. 17 173 | ```sh 174 | example_Fig17_USS_dpw+dpo+Swz_bumpfloods.py 175 | ``` 176 | 177 | The `.py` files are also available as `.ipynb` Jupyter notebooks (generated with jupytext). Respective markdown tags are included in the .py files to generate the formatting e.g. headers in the Jupyter notebooks. 178 | 179 | 180 |

(back to top)

181 | 182 | 183 | ### Interpretation of Unsteady-state drainage and imbibition experiments 184 | We include 4 Jupyter notebooks with synthetic data for drainage and imbibition which are based on the example_Fig17_USS_dpw+dpo+Swz_bumpfloods.ipynb. In the "_to-Excel" notebooks the "simulated" experimental data is written to Excel files, then loaded again and interpreted by inverse modelling. In the "_from-Excel" notebooks only the data from the Excel sheets is loaded and then interpreted by inverse modelling. 185 | 186 | * Drainage 187 | ```sh 188 | USS_synthetic_data_drainage_to-Excel.ipynb 189 | ``` 190 | which generates expdataHISUSSdrainage.xlsx 191 | ```sh 192 | USS_synthetic_data_drainage_from-Excel.ipynb 193 | ``` 194 | * Imbibition 195 | ```sh 196 | USS_synthetic_data_imbibition_to-Excel.ipynb 197 | ``` 198 | which generates expdataHISUSSimbibition.xlsx 199 | ```sh 200 | USS_synthetic_data_imbibition_from-Excel.ipynb 201 | ``` 202 | 203 | The Excel files and "_from-Excel" notebooks can be used to interpret user experimental data sets. In principle only the Excel sheets have to be modified with the user experimental data sets. But it is important to maintain the format given in the Excel sheets. 204 | 205 | In these notebooks two alternative methods for assessing the uncertainty regions have been added: 206 | 1. for each varied fit parameter a normal distribution is generated with 1000 samples and then from these samples mean and standard deviation are determined and plotted as uncertainty ranges 207 | 2. Making use of the 208 | numpy.random.Generator.multivariate_normal function, the covariance matrix from lmfit is directly used to draw random samples. With a few modifications, i.e. not allowing negative values for fit parameters that should not become negative, 95% confidence intervals are generated, which is the cleanest way. 209 | 210 | Interestingly, the uncertainty ranges from the proper 95% confidence intervals are not dramatically different from the guestimate used in the previous examples. 211 | 212 | Also, plotting the correlation matrix has been improved where the varied parameters are automatically determined from the lmfit report. 213 | 214 | 215 | 216 | 217 | ### Interpretation of Steady-state imbibition experiments 218 | We include 2 Jupyter notebooks with synthetic data for imbibition for steady-state flow derived from examples in Transport in Porous Media 140(1), 27-57, 2021. 219 | In the "_to-Excel" notebooks the "simulated" experimental data is written to Excel files, then loaded again and interpreted by inverse modelling. In the "_from-Excel" notebooks only the data from the Excel sheets is loaded and then interpreted by inverse modelling. 220 | 221 | 222 | * Imbibition 223 | ```sh 224 | SS_synthetic_data_imbibition_to-Excel.ipynb 225 | ``` 226 | which generates expdataHISSSimbibition.xlsx 227 | ```sh 228 | SS_synthetic_data_imbibition_from-Excel.ipynb 229 | ``` 230 | 231 | The Excel files and "_from-Excel" notebooks can be used to interpret user experimental data sets. In principle only the Excel sheets have to be modified with the user experimental data sets. But it is important to maintain the format given in the Excel sheets. 232 | 233 | 234 | 235 | 236 | ## Roadmap 237 | 238 | - [ ] Add Changelog 239 | - [ ] Add more examples from previous papers 240 | - [ ] steady-state experiments 241 | - [ ] matching real data 242 | 243 | 246 | 247 |

(back to top)

248 | 249 | 250 | 251 | ## Contributing 252 | 253 | It would be great if you could contribute to this project. Any contributions you make are **greatly appreciated**. 254 | 255 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 256 | Don't forget to give the project a star! Thanks again! 257 | 258 | 259 |

(back to top)

260 | 261 | 262 | 263 | 264 | ## License 265 | 266 | Distributed under the MIT License. See `LICENSE.txt` for more information. 267 | 268 |

(back to top)

269 | 270 | 271 | 272 | 273 | ## Contact 274 | 275 | Steffen Berg - LinkedIn - steffen.berg@shell.com 276 | 277 | Project Link: [https://github.com/sede-open/Core2Relperm](https://github.com/sede-open/Core2Relperm) 278 | 279 |

(back to top)

280 | 281 | 282 | 283 | 284 | ## Acknowledgments 285 | 286 | We would like to acknowledge 287 | 288 | * Sherin Mirza, Aarthi Thyagarajan and Luud Heck from Shell supporting the OpenSource release on GitHub 289 | * Vishal Ahuja supporting the project in general and also the initial release on Github. 290 | * Holger Ott, Omidreza Amrollahinasab (University of Leoben), and Jos Maas (PanTerra) for helpful discussions 291 | * Tibi Sorop and Yingxue Wang for reviewing the paper manuscript 292 | * Daan de Kort for providing an updated relperm uncertainty quantification making use of the covariance matrix. 293 | 294 |

(back to top)

295 | 296 | 297 | 298 | 299 | 300 | ## How to Cite 301 | 302 | 1. S. Berg, H. Dijk, E. Unsal, R. Hofmann, B. Zhao, V. Ahuja, Simultaneous Determination of Relative Permeability and Capillary Pressure from an Unsteady-State Core Flooding Experiment ? Computers and Geotechnics 168, 106091, 2024. 303 | 304 | 2. S. Berg, E. Unsal, H. Dijk, Non-Uniqueness and Uncertainty Quantification of Relative Permeability Measurements by Inverse Modelling, Computers and Geotechnics 132, 103964, 2021. 305 | 306 | 3. S. Berg, E. Unsal, H. Dijk, Sensitivity and uncertainty analysis for parameterization of multi phase flow models, Transport in Porous Media 140(1), 27-57, 2021. 307 | 308 | 309 | 310 |

(back to top)

311 | 312 | 313 | 314 | 315 | 316 | 317 | [product-screenshot]: images/Core2Relperm-logo.png 318 | [license-url]: https://github.com/sede-open/Core2Relperm/blob/main/license.txt 319 | [linkedin-url]: www.linkedin.com/in/steffen-berg-5409a672 320 | [contributors-url]: https://github.com/sede-open/Core2Relperm/graphs/contributors 321 | [forks-url]: https://github.com/sede-open/Core2Relperm/network/members 322 | [issues-url]: https://github.com/sede-open/Core2Relperm/issues 323 | [stars-url]: https://github.com/sede-open/Core2Relperm/stargazers 324 | [BestReadme-url]: https://github.com/othneildrew/Best-README-Template 325 | 326 | -------------------------------------------------------------------------------- /environment-fromhistory.yml: -------------------------------------------------------------------------------- 1 | name: relperm 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - asteval==1.0.2=pyhd8ed1ab_0 6 | - asttokens==2.4.1=pyhd8ed1ab_0 7 | - brotli-bin==1.1.0=hcfcfb64_1 8 | - brotli==1.1.0=hcfcfb64_1 9 | - bzip2==1.0.8=h2466b09_7 10 | - ca-certificates==2024.12.14 11 | - cairo==1.18.0=h32b962e_3 12 | - certifi==2024.8.30 13 | - colorama==0.4.6=pyhd8ed1ab_0 14 | - comm==0.2.2=pyhd8ed1ab_0 15 | - contourpy==1.2.1=py312h0d7def4_0 16 | - cycler==0.12.1=pyhd8ed1ab_0 17 | - debugpy==1.8.5=py312h275cf98_0 18 | - decorator==5.1.1=pyhd8ed1ab_0 19 | - dill==0.3.8=pyhd8ed1ab_0 20 | - double-conversion==3.3.0=h63175ca_0 21 | - emcee==3.1.6=pyhd8ed1ab_0 22 | - exceptiongroup==1.2.2=pyhd8ed1ab_0 23 | - executing==2.0.1=pyhd8ed1ab_0 24 | - expat==2.6.2=h63175ca_0 25 | - font-ttf-dejavu-sans-mono==2.37=hab24e00_0 26 | - font-ttf-inconsolata==3.000=h77eed37_0 27 | - font-ttf-source-code-pro==2.038=h77eed37_0 28 | - font-ttf-ubuntu==0.83=h77eed37_2 29 | - fontconfig==2.14.2=hbde0cde_0 30 | - fonts-conda-ecosystem==1=0 31 | - fonts-conda-forge==1=0 32 | - fonttools==4.53.1=py312h4389bb4_0 33 | - freetype==2.12.1=hdaf720e_2 34 | - future==1.0.0=pyhd8ed1ab_0 35 | - graphite2==1.3.13=h63175ca_1003 36 | - harfbuzz==9.0.0=h2bedf89_1 37 | - icu==75.1=he0c23c2_0 38 | - importlib-metadata==8.2.0=pyha770c72_0 39 | - importlib_metadata==8.2.0=hd8ed1ab_0 40 | - intel-openmp==2024.2.0=h57928b3_980 41 | - ipykernel==6.29.5=pyh4bbf305_0 42 | - ipython==8.26.0=pyh7428d3b_0 43 | - jedi==0.19.1=pyhd8ed1ab_0 44 | - jupyter_client==8.6.2=pyhd8ed1ab_0 45 | - jupyter_core==5.7.2=py312h2e8e312_0 46 | - kiwisolver==1.4.5=py312h0d7def4_1 47 | - krb5==1.21.3=hdf4eb48_0 48 | - lcms2==2.16=h67d730c_0 49 | - lerc==4.0.0=h63175ca_0 50 | - libblas==3.9.0=23_win64_mkl 51 | - libbrotlicommon==1.1.0=hcfcfb64_1 52 | - libbrotlidec==1.1.0=hcfcfb64_1 53 | - libbrotlienc==1.1.0=hcfcfb64_1 54 | - libcblas==3.9.0=23_win64_mkl 55 | - libclang13==18.1.8=default_ha5278ca_1 56 | - libdeflate==1.21=h2466b09_0 57 | - libexpat==2.6.2=h63175ca_0 58 | - libffi==3.4.2=h8ffe710_5 59 | - libglib==2.80.3=h7025463_1 60 | - libhwloc==2.11.1=default_h8125262_1000 61 | - libiconv==1.17=hcfcfb64_2 62 | - libintl==0.22.5=h5728263_2 63 | - libjpeg-turbo==3.0.0=hcfcfb64_1 64 | - liblapack==3.9.0=23_win64_mkl 65 | - libpng==1.6.43=h19919ed_0 66 | - libsodium==1.0.18=h8d14728_1 67 | - libsqlite==3.46.0=h2466b09_0 68 | - libtiff==4.6.0=hb151862_4 69 | - libwebp-base==1.4.0=hcfcfb64_0 70 | - libxcb==1.16=hcd874cb_0 71 | - libxml2==2.12.7=h0f24e4e_4 72 | - libxslt==1.1.39=h3df6e99_0 73 | - libzlib==1.3.1=h2466b09_1 74 | - llvmlite==0.43.0=py312h1f7db74_0 75 | - lmfit==1.3.2=pyhd8ed1ab_0 76 | - m2w64-gcc-libgfortran==5.3.0=6 77 | - m2w64-gcc-libs-core==5.3.0=7 78 | - m2w64-gcc-libs==5.3.0=7 79 | - m2w64-gmp==6.1.0=2 80 | - m2w64-libwinpthread-git==5.0.0.4634.697f757=2 81 | - matplotlib-base==3.9.1=py312h90004f6_2 82 | - matplotlib-inline==0.1.7=pyhd8ed1ab_0 83 | - matplotlib==3.9.1=py312h2e8e312_2 84 | - mkl==2024.1.0=h66d3029_694 85 | - msys2-conda-epoch==20160418=1 86 | - munkres==1.1.4=pyh9f0ad1d_0 87 | - nest-asyncio==1.6.0=pyhd8ed1ab_0 88 | - numba==0.60.0=py312hcccf92d_0 89 | - numpy==2.0.1=py312h49bc9c5_0 90 | - openjpeg==2.5.2=h3d672ee_0 91 | - openssl==3.4.0 92 | - packaging==24.1=pyhd8ed1ab_0 93 | - pandas==2.2.2=py312h72972c8_1 94 | - parso==0.8.4=pyhd8ed1ab_0 95 | - patsy==0.5.6=pyhd8ed1ab_0 96 | - pcre2==10.44=h3d7b363_0 97 | - pickleshare==0.7.5=py_1003 98 | - pillow==10.4.0=py312h381445a_0 99 | - pip==24.2=pyhd8ed1ab_0 100 | - pixman==0.43.4=h63175ca_0 101 | - platformdirs==4.2.2=pyhd8ed1ab_0 102 | - prompt-toolkit==3.0.47=pyha770c72_0 103 | - psutil==6.0.0=py312h4389bb4_0 104 | - pthread-stubs==0.4=hcd874cb_1001 105 | - pthreads-win32==2.9.1=hfa6e2cd_3 106 | - pure_eval==0.2.3=pyhd8ed1ab_0 107 | - pygments==2.18.0=pyhd8ed1ab_0 108 | - pyparsing==3.1.2=pyhd8ed1ab_0 109 | - pyside6==6.7.2=py312h2ee7485_2 110 | - python-dateutil==2.9.0=pyhd8ed1ab_0 111 | - python-tzdata==2024.1=pyhd8ed1ab_0 112 | - python==3.12.4=h889d299_0_cpython 113 | - python_abi==3.12=4_cp312 114 | - pytz==2024.1=pyhd8ed1ab_0 115 | - pywin32==306=py312h53d5487_2 116 | - pyzmq==26.1.0=py312hd7027bb_0 117 | - qhull==2020.2=hc790b64_5 118 | - qt6-main==6.7.2=hbb46ec1_4 119 | - scipy==1.14.0=py312h1f4e10d_1 120 | - seaborn-base==0.13.2=pyhd8ed1ab_2 121 | - seaborn==0.13.2=hd8ed1ab_2 122 | - setuptools-scm==8.1.0=pyhd8ed1ab_0 123 | - setuptools==72.1.0=pyhd8ed1ab_0 124 | - six==1.16.0=pyh6c4a22f_0 125 | - stack_data==0.6.2=pyhd8ed1ab_0 126 | - statsmodels==0.14.2=py312h1a27103_0 127 | - tbb==2021.12.0=hc790b64_3 128 | - tk==8.6.13=h5226925_1 129 | - tomli==2.0.1=pyhd8ed1ab_0 130 | - tornado==6.4.1=py312h4389bb4_0 131 | - traitlets==5.14.3=pyhd8ed1ab_0 132 | - typing-extensions==4.12.2=hd8ed1ab_0 133 | - typing_extensions==4.12.2=pyha770c72_0 134 | - tzdata==2024a=h0c530f3_0 135 | - ucrt==10.0.22621.0=h57928b3_0 136 | - uncertainties==3.2.2=pyhd8ed1ab_1 137 | - vc14_runtime==14.40.33810=ha82c5b3_20 138 | - vc==14.3=h8a93ad2_20 139 | - vs2015_runtime==14.40.33810=h3bf8584_20 140 | - wcwidth==0.2.13=pyhd8ed1ab_0 141 | - wheel==0.44.0=pyhd8ed1ab_0 142 | - xorg-libxau==1.0.11=hcd874cb_0 143 | - xorg-libxdmcp==1.1.3=hcd874cb_0 144 | - xz==5.2.6=h8d14728_0 145 | - zeromq==4.3.5=he1f189c_4 146 | - zipp==3.19.2=pyhd8ed1ab_0 147 | - zlib==1.3.1=h2466b09_1 148 | - zstd==1.5.6=h0ea2cb4_0 149 | - conda-forge::openpyxl 150 | - astropy 151 | - conda-forge::xlsxwriter 152 | prefix: C:\ProgramData\miniforge3\envs\relperm 153 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: relperm 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - emcee 6 | - lmfit 7 | - matplotlib 8 | - numba 9 | - numpy 10 | - pandas 11 | - scipy 12 | - seaborn 13 | - openpyxl 14 | - ipykernel 15 | -------------------------------------------------------------------------------- /images/Core2Relperm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/images/Core2Relperm-logo.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/images/logo.png -------------------------------------------------------------------------------- /python/example_Fig09_USS_dpw+dpo+noSwz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #------------------------------------------------------------------------------------------------ 4 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 5 | # Licensed under the MIT License. See License.txt in the project root for license information. 6 | #------------------------------------------------------------------------------------------------ 7 | 8 | 9 | # %% [markdown] 10 | """ 11 | example_Fig09_USS_dpw+dpo+noSwz.py 12 | 13 | # Fig. 09 from Berg et al. Simultaneous Determination of Relative Permeability and Capillary Pressure ... paper 14 | 15 | 16 | Inverse Modeling of Unsteady-State (USS) Core Flooding Experiments in Special Core Analysis (SCAL) 17 | for the purpose of extracting relative permeability and capillary pressure-saturation functions 18 | 19 | Created on 22.11.2021 20 | by Harm Dijk, Steffen Berg 21 | 22 | based on example004_USS_match_using_LET_and Skjaeveland_v001.py by Harm Dijk 23 | 24 | 25 | * using LET model for relative permeability parameterization 26 | * using Skjaeveland model for capillary pressure parameterization 27 | * match production curve, pressure drop vs. time 28 | * NO saturation profiles included in match 29 | * ONE flow rate ONLY 30 | 31 | """ 32 | # %% 33 | 34 | # --- 35 | # jupyter: 36 | # jupytext: 37 | # formats: ipynb,py:percent 38 | # text_representation: 39 | # extension: .py 40 | # format_name: percent 41 | # format_version: '1.3' 42 | # jupytext_version: 1.13.0 43 | # kernelspec: 44 | # display_name: Python 3 45 | # language: python 46 | # name: python3 47 | # --- 48 | 49 | # %% [markdown] 50 | """ 51 | # Dependencies 52 | """ 53 | # %% 54 | 55 | import seaborn as sns 56 | sns.set_style('whitegrid') 57 | import pandas as pd 58 | 59 | import numpy as np 60 | import matplotlib.pyplot as plt 61 | 62 | plt.rc('figure',facecolor='white') 63 | 64 | from scallib001.displacementmodel1D2P001 import DisplacementModel1D2P 65 | import scallib001.relpermlib001 as rlplib 66 | 67 | # %% [markdown] 68 | """ 69 | # generate synthetic data set 70 | """ 71 | # %% 72 | 73 | # relperm and pc 74 | 75 | KRWE = 0.7 76 | KROE = 1.0 77 | SWC = 0.08 78 | SORW = 0.14 79 | NW = 2.5 80 | NOW = 3.0 81 | 82 | rlp_model1 = rlplib.Rlp2PCorey( SWC, SORW, NW, NOW, KRWE, KROE ) 83 | 84 | 85 | #Define the Skjaeveland Pc model 86 | #extended with Masalmeh linear slope, parameter 'ci' 87 | 88 | 89 | class Skjaeveland: 90 | 91 | def __init__(self,Swc,Sorw,Swi,cw,aw,ao,si=0): 92 | 93 | self.Swc = Swc 94 | self.Sorw = Sorw 95 | self.Swi = Swi 96 | self.cw = cw 97 | self.aw = aw 98 | self.ao = ao 99 | 100 | ssw = ( Swi-Swc )/(1-Swc ) 101 | sso = (1-Swi-Sorw)/(1-Sorw) 102 | co = - cw*np.power(sso,ao)/np.power(ssw,aw) 103 | 104 | self.co = co 105 | self.si = si 106 | 107 | def calc(self,swv): 108 | Swc = self.Swc 109 | Sorw = self.Sorw 110 | 111 | sov = 1-swv 112 | ssw = (swv-Swc )/(1-Swc ) 113 | sso = (sov-Sorw)/(1-Sorw) 114 | 115 | l = (swv-self.Swi) * self.si * -1 116 | 117 | return self.cw/np.power(ssw,self.aw) + self.co/np.power(sso,self.ao) + l 118 | 119 | 120 | #cpr_model1 = rlplib.CubicInterpolator( 121 | # np.array([0.08,0.1,0.15,0.3,0.4,0.5,0.59,0.68,0.73,0.78,0.81,0.82,0.859,0.86]), 122 | # np.array([-0.0,-0.001,-0.0054,-0.015,-0.0193,-0.0241,-0.0435,-0.0923,-0.1284,-0.18,-0.21,-0.24,-0.9,-2.0]) 123 | # ) 124 | 125 | # make new pc model 126 | AWI = 0.30 127 | AOI = 0.60*1.5 128 | CWI = 11.0/1000 129 | SWI = 0.13 130 | CI = 0.0 131 | 132 | Sorwi = SORW - 0.01 133 | skj_model = Skjaeveland( SWC, Sorwi, SWI, CWI, AWI, AOI, CI ) 134 | EPS = 0.001 135 | n = 101 136 | swvi = np.linspace( skj_model.Swc+EPS, 1-Sorwi-EPS, n ) 137 | pcvi = skj_model.calc(swvi) 138 | cpr_model1 = rlplib.CubicInterpolator( swvi, pcvi, lex=1, rex=1 ) 139 | 140 | 141 | # %% [markdown] 142 | """ 143 | # Plot Ground Truth relative permeability kr(Sw) and capillary pressure pc(Sw) 144 | """ 145 | # %% 146 | swv = np.linspace(0,1,101) 147 | 148 | plt.figure(figsize=(15,4)) 149 | plt.subplot(1,2,1) 150 | plt.plot( swv, rlp_model1.calc_kr1(swv), 'b', label='krw') 151 | plt.plot( swv, rlp_model1.calc_kr2(swv), 'r', label='kro') 152 | plt.legend(); 153 | plt.title('relative permeability'); 154 | plt.xlabel('saturation Sw'); plt.ylabel('relative permeability'); 155 | 156 | plt.subplot(1,2,2) 157 | plt.plot( swv, cpr_model1.calc(swv)[0] ) 158 | plt.ylim(-2,2) 159 | plt.title('Water imbibition Pc [bar]'); 160 | plt.xlabel('saturation Sw'); plt.ylabel('capillary pressure [bar]'); 161 | 162 | plt.show() 163 | 164 | 165 | # core and fluid data 166 | 167 | exp_core_length = 3.9 # cm 168 | exp_core_area = 12.0 # cm2 169 | exp_permeability = 100.0 # mDarcy 170 | exp_porosity = 0.18 # v/v 171 | exp_sw_initial = SWC # v/v 172 | exp_viscosity_w = 1.0 # cP 173 | exp_viscosity_n = 1.0 # cP 174 | exp_density_w = 1000.0 # kg/m3 175 | exp_density_n = 1000.0 176 | 177 | 178 | # schedules 179 | 180 | # T_End in dimensionless time 181 | # tD_conv = (u.hour * u.cm**3/u.minute / (L*A*por)).to(u.minute/u.minute) 182 | 183 | T_END = 2.0 # hours 184 | 185 | Movie_times = np.linspace(0,T_END,100) 186 | 187 | #Schedule = pd.DataFrame( 188 | # [[0.0, 0.1, 1.0], 189 | # [23.945, 0.5, 1.0], 190 | # [29.08, 2.0, 1.0], 191 | # [31.24, 5.0, 1.0]], 192 | # columns=['StartTime','InjRate','FracFlow'] ) 193 | 194 | 195 | Schedule = pd.DataFrame( 196 | [[0.0, 0.1, 1.0]], 197 | columns=['StartTime','InjRate','FracFlow'] ) 198 | #times in min, injrate in cm3/min 199 | 200 | 201 | #Define 1D2P displacement model 202 | 203 | model1 = DisplacementModel1D2P( 204 | NX=50, 205 | core_length = exp_core_length, 206 | core_area = exp_core_area, 207 | permeability = exp_permeability, 208 | porosity = exp_porosity, 209 | sw_initial = exp_sw_initial, 210 | viscosity_w = exp_viscosity_w, 211 | viscosity_n = exp_viscosity_n, 212 | density_w = exp_density_w, 213 | density_n = exp_density_n, 214 | rlp_model = rlp_model1, 215 | cpr_model = cpr_model1, 216 | time_end = T_END, 217 | rate_schedule = Schedule, 218 | movie_schedule = Movie_times, # Same timesteps as experimental data 219 | ) 220 | 221 | 222 | #Solve the model with the initial gues relperm and capcurve models 223 | #note that first time code needs to be called by numba, takes some time 224 | model1.solve(); 225 | 226 | #Keep the results 227 | result_exp = model1.results 228 | 229 | print(result_exp.keys()) 230 | print(result_exp.tss_table.keys()) 231 | 232 | #plot model results 233 | #1D2P timestep summary data stored in 'tss_table', with column names similar to MoReS 234 | 235 | 236 | # %% [markdown] 237 | """ 238 | # Plot pressure drop, production curve and saturation profiles generated from ground truth 239 | """ 240 | # %% 241 | 242 | plt.figure(figsize=(15,4)) 243 | tss = result_exp.tss_table 244 | plt.subplot(1,2,1) 245 | plt.plot( tss.TIME, tss.WATERProd, 'b', label='1D sim' ) 246 | plt.plot( tss.TIME, tss.OILProd, 'r', label='1D sim' ) 247 | plt.legend(); 248 | plt.ylabel('Oil and water rate[cm3/minute]') 249 | plt.xlabel('Time [hour]'); 250 | plt.subplot(1,2,2) 251 | plt.plot( tss.TIME, tss.CumWATER, 'b', label='CumWATER prd 1D sim' ) 252 | plt.plot( tss.TIME, tss.CumOIL, 'r', label='CumOIL prd 1D sim' ) 253 | plt.plot( tss.TIME, tss.CumWATER+tss.CumOIL, 'black', label='CumWater + CumOIL prd 1D sim' ) 254 | plt.legend(); 255 | plt.ylabel('Oil and water volume [cm3]') 256 | plt.xlabel('Time [hour]'); 257 | plt.show() 258 | 259 | 260 | plt.figure(figsize=(15,4)) 261 | plt.subplot(1,2,1) 262 | tss = result_exp.tss_table 263 | plt.plot( tss.TIME, tss.delta_P_w,'b--', label='delta_P_w 1D sim') 264 | plt.plot( tss.TIME, tss.delta_P_o,'r--', label='delta_P_o 1D sim' ); 265 | plt.xlabel('Time [hour]'); 266 | plt.ylabel('Pressure drop [bar]'); 267 | plt.title('Pressure drop [bar]') 268 | plt.legend(loc='upper left'); 269 | plt.subplot(1,2,2) 270 | plt.plot( result_exp.movie_time, result_exp.movie_sw[:, 0],'r', label='1D sim entry face') 271 | plt.plot( result_exp.movie_time, result_exp.movie_sw[:,-1],'b', label='1D sim exit face' ) 272 | plt.legend(loc='upper left'); 273 | plt.title('Sw at entry and exit face [v/v]') 274 | plt.xlabel('Time [hour]'); 275 | plt.ylabel('Sw [v/v]'); 276 | plt.show() 277 | 278 | plt.figure(figsize=(15,4)) 279 | plt.plot( result_exp.xD, result_exp.movie_sw.T, 'k'); 280 | plt.xlabel('xD [dimless]') 281 | plt.ylabel('Sw [v/v]') 282 | plt.title('Sw profiles at requested times'); 283 | plt.show() 284 | 285 | 286 | 287 | # add noise to relperm and pc models (starting values for fit) 288 | Nkr=15 289 | swvNkr = np.linspace(SWC,1-SORW,Nkr) 290 | Indexv=np.linspace(1,Nkr,Nkr) 291 | 292 | krerrlevel=0.1 # add 10% noise 293 | np.random.seed(123) 294 | krexpdata={'INDEX':Indexv,'Sat':swvNkr,'kr1':(rlp_model1.calc_kr1(swvNkr)+krerrlevel*(np.random.rand(Nkr)-0.5)),'kr2':(rlp_model1.calc_kr2(swvNkr)+krerrlevel*(np.random.rand(Nkr)-0.5))} 295 | krexp=pd.DataFrame(data=krexpdata) 296 | 297 | pcerrlevel=krerrlevel*np.abs(cpr_model1.calc(swvNkr)[0]).max() 298 | pcexpdata=krexpdata={'INDEX':Indexv,'Sat':swvNkr,'Pc':(cpr_model1.calc(swvNkr)[0]+pcerrlevel*(np.random.rand(Nkr)-0.5))} 299 | pcexp=pd.DataFrame(data=pcexpdata) 300 | 301 | swv = np.linspace(0,1,101) 302 | 303 | plt.figure(figsize=(15,4)) 304 | plt.subplot(1,2,1) 305 | plt.plot( swv, rlp_model1.calc_kr1(swv), 'b', label='krw ground truth') 306 | plt.plot( swv, rlp_model1.calc_kr2(swv), 'r', label='kro ground truth') 307 | plt.plot( krexp.Sat, krexp.kr1, 'o', color='blue', label='krw with noise') 308 | plt.plot( krexp.Sat, krexp.kr2, 'o', color='red', label='kro with noise') 309 | plt.legend(); 310 | plt.title('relative permeability'); 311 | plt.xlabel('saturation Sw'); plt.ylabel('relative permeability'); 312 | 313 | plt.subplot(1,2,2) 314 | plt.plot( swv, cpr_model1.calc(swv)[0], label='pc ground truth') 315 | plt.plot( pcexp.Sat, pcexp.Pc, 'o', color='black', label='pc with noise') 316 | plt.ylim(-2,2) 317 | plt.title('Water imbibition Pc [bar]'); 318 | plt.xlabel('saturation Sw'); plt.ylabel('capillary pressure [bar]'); 319 | plt.legend(); 320 | plt.show() 321 | 322 | 323 | # %% [markdown] 324 | """ 325 | # Add noise to ground truth 326 | """ 327 | # %% 328 | 329 | # add some noise to "experimental data" 330 | Nrandt = len(result_exp.tss_table) 331 | Nrandx = len(result_exp.xD) 332 | 333 | errlevel=0.1 # add 10% noise 334 | dpmean= (tss.delta_P_w.mean()+tss.delta_P_o.mean())/2 335 | Cummean = (tss.CumWATER.mean()+tss.CumOIL.mean())/2 336 | Swmean = result_exp.movie_sw.T.mean() 337 | 338 | np.random.seed(123) 339 | dpwerr = tss.delta_P_w + dpmean*errlevel*(np.random.rand(Nrandt)-0.5) 340 | dpoerr = tss.delta_P_o + dpmean*errlevel*(np.random.rand(Nrandt)-0.5) 341 | 342 | CumWATERerr = tss.CumWATER + Cummean*errlevel*(np.random.rand(Nrandt)-0.5) 343 | CumOILerr = tss.CumOIL + Cummean*errlevel*(np.random.rand(Nrandt)-0.5) 344 | 345 | 346 | # this works but adds same error to each time step at same position which is a bit artificial 347 | #Swproferr = result_exp.movie_sw.T + Swmean * errlevel * (np.random.rand(Nrandx)-0.5)[:,np.newaxis] 348 | 349 | Swerrmaxtrix=np.full_like(result_exp.movie_sw.T,1) 350 | for i in range(0,result_exp.movie_sw.T.shape[1]): 351 | Swerrmaxtrix[:,i]=Swmean * errlevel * (np.random.rand(Nrandx)-0.5) 352 | Swproferr = result_exp.movie_sw.T+Swerrmaxtrix 353 | Swaverr = np.average(Swproferr,axis=0) 354 | 355 | 356 | plt.figure(figsize=(15,4)) 357 | plt.subplot(1,2,1) 358 | plt.plot( tss.TIME, tss.CumWATER, 'b--', label='CumWATER prd 1D sim' ) 359 | plt.plot( tss.TIME, CumWATERerr, 'b', label='CumWATER prd 1D sim + noise' ) 360 | plt.plot( tss.TIME, tss.CumOIL, 'r--', label='CumOIL prd 1D sim' ) 361 | plt.plot( tss.TIME, CumOILerr, 'r', label='CumOIL prd 1D sim + noise' ) 362 | plt.legend(); 363 | plt.ylabel('Oil and water volume [cm3]') 364 | plt.xlabel('Time [hour]'); 365 | plt.subplot(1,2,2) 366 | tss = result_exp.tss_table 367 | plt.plot( tss.TIME, tss.delta_P_w,'b--', label='delta_P_w 1D sim') 368 | plt.plot( tss.TIME, dpwerr,'b', label='delta_P_w 1D sim + noise') 369 | plt.plot( tss.TIME, tss.delta_P_o,'r--', label='delta_P_o 1D sim' ); 370 | plt.plot( tss.TIME, dpoerr,'r', label='delta_P_o 1D sim + noise' ); 371 | plt.xlabel('Time [hour]'); 372 | plt.ylabel('Pressure drop [bar]'); 373 | plt.title('Pressure drop [bar]') 374 | plt.legend(loc='upper left'); 375 | plt.show() 376 | 377 | plt.figure(figsize=(15,4)) 378 | plt.plot( result_exp.xD, result_exp.movie_sw.T, 'k--'); 379 | plt.plot( result_exp.xD, Swproferr, 'k'); 380 | plt.xlabel('xD [dimless]') 381 | plt.ylabel('Sw [v/v]') 382 | plt.title('Sw profiles at requested times'); 383 | plt.show() 384 | 385 | 386 | # writing to expdataHIS data frame 387 | 388 | # without noise 389 | # expdataHIS = result_exp.tss_table 390 | 391 | #with noise 392 | 393 | Indext=np.linspace(1,Nrandt,Nrandt) 394 | OIIP=exp_core_length*exp_core_area*exp_porosity*(1-exp_sw_initial) 395 | 396 | # Defnition of dp_switch_time 397 | # dp_sim = np.where( dp_time dp= dp_w 399 | 400 | 401 | expdataHISdata={'INDEX':Indext,'TIME':tss.TIME, 'PVinj':tss.PVinj, 'Sw':Swaverr, 'RF':(CumOILerr/OIIP), 'CumOIL':CumOILerr, 'DeltaPressure':dperr, 'dpw':dpwerr, 'dpo':dpoerr, 'tD':result_exp.tss_table.tD} 402 | expdataHIS=pd.DataFrame(data=expdataHISdata) 403 | 404 | 405 | # writing saturation profiles to 406 | 407 | sattimelistn = [1,4,8,12,20] 408 | expdataHISsattimes = tss.TIME[sattimelistn] 409 | satprofileindex = np.linspace(1,len(result_exp.xD),len(result_exp.xD)) 410 | expdataHISsatprofilesdata = {'INDEX':satprofileindex, 'Distance':result_exp.xD * exp_core_length, 'xD':result_exp.xD} 411 | for i in range(len(sattimelistn)): 412 | expdataHISsatprofilesdata['Profile'+str(i+1)]=result_exp.movie_sw.T[:,sattimelistn[i]] 413 | expdataHISsatprofiles=pd.DataFrame(data=expdataHISsatprofilesdata) 414 | 415 | 416 | 417 | # %% [markdown] 418 | """ 419 | # inverse modeling: start with initial guess 420 | """ 421 | # %% 422 | 423 | #Fit LET model to the estimate from the experimental data 424 | #to have starting point for fitting of USS experiment below 425 | 426 | from lmfit import Minimizer, Parameters, report_fit 427 | 428 | class RlpMatchObjective1: 429 | 430 | def __init__(self, kr_data ): 431 | 432 | self.kr_data = kr_data 433 | 434 | self.sw_data = kr_data.Sat.values 435 | self.kw_data = kr_data.kr1.values 436 | self.ko_data = kr_data.kr2.values 437 | 438 | self.errorbar = 0.02 439 | 440 | self.counter = 0 441 | 442 | def __call__(self, params): 443 | '''This function is called by the optimizer; calculate mismatch vector.''' 444 | 445 | self.counter += 1 446 | 447 | # Build new relperm model from current parameters 448 | rlpmodeli = self.params_to_rlpmodel( params ) 449 | 450 | # Calculate relperm values at data sw values 451 | kw, ko, _, _ = rlpmodeli.calc(self.sw_data) 452 | 453 | y = np.hstack( [kw, ko ] ) 454 | y_data = np.hstack( [self.kw_data, self.ko_data] ) 455 | 456 | return (y-y_data)/self.errorbar 457 | 458 | def params_to_rlpmodel( self, params ): 459 | 460 | v = params.valuesdict() 461 | 462 | Swc = v['Swc'] 463 | Sorw = v['Sorw'] 464 | krwe = v['krwe'] 465 | kroe = v['kroe'] 466 | Lw = v['Lw'] 467 | Ew = v['Ew'] 468 | Tw = v['Tw'] 469 | Lo = v['Lo'] 470 | Eo = v['Eo'] 471 | To = v['To'] 472 | 473 | rlpmodel = rlplib.Rlp2PLET( Swc, Sorw, Lw, Ew, Tw, 474 | Lo, Eo, To, krwe, kroe) 475 | 476 | return rlpmodel 477 | 478 | 479 | 480 | print("Experimental data kr") 481 | print(krexp) 482 | 483 | 484 | KRWE = krexp.kr1.max() 485 | KROE = krexp.kr2.max() 486 | SWC = krexp.Sat.min() 487 | SORW = 1-krexp.Sat.max() 488 | NW = 2.5 489 | NOW = 3.0 490 | 491 | params_LET = Parameters() 492 | params_LET.add('Swc', value=SWC, vary=False) 493 | params_LET.add('Sorw', value=SORW, min=0.05, max=0.45, vary=True) 494 | params_LET.add('krwe', value=KRWE, min=0.05, max=1.10, vary=True) 495 | params_LET.add('kroe', value=KROE, min=0.05, max=1.10, vary=True) 496 | params_LET.add('Lw', value=NW, min=1.5, max=5.0, vary=True) 497 | params_LET.add('Ew', value=0.01, min=1e-4, max=50.0, vary=True) 498 | params_LET.add('Tw', value=1.50, min=1.0, max=5.0, vary=True) 499 | params_LET.add('Lo', value=NOW, min=1.5, max=5.0, vary=True) 500 | params_LET.add('Eo', value=0.01, min=1e-4, max=50.0, vary=True) 501 | params_LET.add('To', value=1.50, min=1.0, max=5.0, vary=True) 502 | 503 | Kr_data = krexp 504 | rlpmatchobjective1 = RlpMatchObjective1( Kr_data ) 505 | 506 | # Check that match objective function works 507 | rlpmatchobjective1( params_LET ) 508 | 509 | result_LET = Minimizer(rlpmatchobjective1, params_LET ).least_squares(diff_step=1e-4,verbose=2) 510 | 511 | # for making the iteration stop earlier add ftol=1e-4 512 | #result_LET = Minimizer(rlpmatchobjective1, params_LET ).least_squares(diff_step=1e-4,verbose=2,ftol=1e-4) 513 | 514 | 515 | # %% [markdown] 516 | """ 517 | # print parameters from fit of LET model to relperm guess 518 | """ 519 | # %% 520 | 521 | report_fit(result_LET) 522 | 523 | print('Fitting LET function to kr guess') 524 | result_LET.params.pretty_print() 525 | 526 | rlpmodel_LET_fit = rlpmatchobjective1.params_to_rlpmodel( result_LET.params ) 527 | 528 | 529 | # %% [markdown] 530 | """ 531 | # Plot fit of LET model to relperm guess 532 | """ 533 | # %% 534 | 535 | swv = np.linspace(0,1,101) 536 | plt.figure(figsize=(6,4)) 537 | plt.plot( swv, rlpmodel_LET_fit.calc_kr1(swv), 'b', label='krw fit') 538 | plt.plot( swv, rlpmodel_LET_fit.calc_kr2(swv), 'r', label='kro fit') 539 | plt.plot( Kr_data.Sat, Kr_data.kr1, 'bo') 540 | plt.plot( Kr_data.Sat, Kr_data.kr2, 'ro'); 541 | plt.legend(); 542 | plt.title('Fit of LET curve to relperm guess'); 543 | plt.show() 544 | 545 | # %% [markdown] 546 | """ 547 | # plot comparison pressure drop for water and oil 548 | """ 549 | # %% 550 | 551 | # Check whether the fitted kr model runs and compare with ground truth 552 | # Assign LET relperm model to 1D2P and solve again 553 | 554 | model1.rlp_model = rlpmodel_LET_fit 555 | #model1.rlp_model = rlp_model1 556 | 557 | result_upd1 = model1.solve().results 558 | 559 | tss1 = result_exp.tss_table 560 | tss2 = result_upd1.tss_table 561 | 562 | plt.figure(figsize=(15,4)) 563 | plt.subplot(1,2,1) 564 | plt.plot( tss1.TIME, tss1.delta_P_w,'-', label='delta_P_w_ org') 565 | plt.plot( tss2.TIME, tss2.delta_P_w,'-', label='delta_P_w_ LET upd') 566 | plt.xlabel('Time [hour]'); 567 | plt.ylabel('Pressure drop [bar]'); 568 | plt.title('Pressure drop WATER [bar]') 569 | plt.legend(loc='upper left'); 570 | plt.subplot(1,2,2) 571 | plt.plot( tss1.TIME, tss1.delta_P_o,'-', label='delta_P_o org') 572 | plt.plot( tss1.TIME, tss2.delta_P_o,'-', label='delta_P_o LET upd' ); 573 | plt.xlabel('Time [hour]'); 574 | plt.ylabel('Pressure drop [bar]'); 575 | plt.title('Pressure drop OIL [bar]') 576 | plt.legend(loc='upper left'); 577 | plt.show() 578 | 579 | 580 | 581 | 582 | 583 | 584 | #Define USS match objective function 585 | #match pressure drop and Sw profiles 586 | #switch from oil to water pressure at dp_switch_time 587 | #weigh pressure drop vs saturation data 588 | 589 | class USSMatchObjective1: 590 | 591 | def __init__(self, 592 | model, 593 | dp_weight=1, op_weight=1, sw_weight=1, 594 | dp_data=None, dp_error=None, 595 | dp_switch_time = 0, 596 | 597 | op_data=None, op_error=None, 598 | 599 | sw_profile_times=None, 600 | sw_profile_data=None, sw_error=None, 601 | 602 | ): 603 | 604 | self.model = model 605 | 606 | self.dp_weight = dp_weight 607 | self.dp_data = dp_data 608 | self.dp_error = dp_error 609 | 610 | self.dp_switch_time = dp_switch_time 611 | 612 | self.op_weight = op_weight 613 | self.op_data = op_data 614 | self.op_error = op_error 615 | 616 | self.sw_weight = sw_weight 617 | self.sw_profile_times = sw_profile_times 618 | self.sw_profile_data = sw_profile_data 619 | self.sw_error = sw_error 620 | 621 | self.counter = 0 622 | 623 | def __call__(self, params): 624 | '''This function is called by the optimizer; calculate mismatch vector.''' 625 | 626 | self.counter += 1 627 | 628 | # Build new relperm model from current parameters 629 | rlpmodeli = self.params_to_rlpmodel( params ) 630 | 631 | # Build new capillary model from current parameters 632 | cprmodeli = self.params_to_cprmodel( params ) 633 | 634 | model = self.model 635 | 636 | model.rlp_model = rlpmodeli 637 | model.cpr_model = cprmodeli 638 | 639 | results = model.solve().results 640 | 641 | 642 | 643 | tss = results.tss_table 644 | 645 | #--- Pressure match 646 | 647 | # Interpolate simulation to measurement TIME 648 | dp_time = self.dp_data.TIME.values 649 | 650 | dp_w_sim = np.interp( dp_time, tss.TIME.values, tss.delta_P_w.values ) 651 | dp_o_sim = np.interp( dp_time, tss.TIME.values, tss.delta_P_o.values ) 652 | 653 | 654 | 655 | # this is for USS experiments where only the total dp is measured 656 | dp_switch_time = self.dp_switch_time 657 | dp_sim = np.where( dp_timelen(current.sw_sim)-4+1: 1039 | plt.xlabel('xD [-]') 1040 | 1041 | if plt.gca().is_first_col(): 1042 | plt.ylabel('Sw [v/v]'); 1043 | 1044 | # TODO generalize time 1045 | v = current.sw_time[counter-2] 1046 | plt.title('Sw at %7.3f hour'%v) 1047 | 1048 | plt.subplots_adjust(hspace=0.3) 1049 | plt.show() 1050 | 1051 | 1052 | def plot_match_dp_production(uss_matchobj,suptitle,draw_phase_pressures=True): 1053 | current = uss_matchobj.current 1054 | 1055 | tss = current.results.tss_table 1056 | 1057 | plt.figure(figsize=(15,4)) 1058 | 1059 | plt.suptitle(suptitle, fontsize=16 ) 1060 | 1061 | plt.subplot(1,2,1) 1062 | #plt.plot( current.dp_time, current.dp_sim, 'r', label='match') 1063 | #plt.plot( current.dp_time, current.dp_dat, 'k', alpha=0.4, label='data') 1064 | plt.plot( current.dp_time, current.dpw_dat, 'b-', label='dP water exp') 1065 | plt.plot( current.dp_time, current.dpo_dat, 'r-', label='dP oil exp') 1066 | 1067 | if draw_phase_pressures: 1068 | plt.plot( tss.TIME, tss.delta_P_w, 'b--', label='dP water sim') 1069 | plt.plot( tss.TIME, tss.delta_P_o, 'r--', label='dP oil sim') 1070 | 1071 | plt.legend(loc='best') 1072 | plt.title('Pressure drop [bar]') 1073 | plt.ylabel('Delta P[bar]') 1074 | plt.xlabel('Time [hour]') 1075 | 1076 | plt.subplot(1,2,2) 1077 | plt.title('Oil production') 1078 | plt.plot( current.op_time, current.op_dat, 'k-', label='Oil data') 1079 | plt.plot( current.op_time, current.op_sim, 'r--', label='Oil match') 1080 | plt.xlabel('Time [hour]'); 1081 | plt.ylabel('Oil production [cm3]') 1082 | plt.show() 1083 | 1084 | 1085 | def plot_match_production(uss_matchobj,suptitle): 1086 | 1087 | current = uss_matchobj.current 1088 | 1089 | tss = current.results.tss_table 1090 | 1091 | plt.figure(figsize=(15,4)) 1092 | 1093 | plt.suptitle(suptitle, fontsize=16 ) 1094 | 1095 | plt.subplot(1,2,1) 1096 | plt.title('Oil production') 1097 | plt.plot( current.op_time, current.op_dat, 'k-', label='Oil data') 1098 | plt.plot( current.op_time, current.op_sim, 'r--', label='Oil match') 1099 | plt.xlabel('Time [hour]'); 1100 | plt.ylabel('Oil production [cm3]') 1101 | 1102 | plt.subplot(1,2,2) 1103 | 1104 | plt.plot( current.op_time, current.op_mismatch, 'r' ) 1105 | plt.title('Oil production standardized mismatch: (sim-data)/stderr') 1106 | plt.plot( current.op_time, current.op_mismatch, 'r') 1107 | plt.axhline(+1,color='k',ls='--') 1108 | plt.axhline(-1,color='k',ls='--'); 1109 | 1110 | plt.xlabel('Time [hour]'); 1111 | plt.show() 1112 | 1113 | 1114 | def plot_match_all(matchobj,suptitle): 1115 | 1116 | plot_rlp_cpr( matchobj, suptitle ) 1117 | plt.show() 1118 | plot_match_dp( matchobj, suptitle, draw_phase_pressures=True ) 1119 | plt.show() 1120 | plot_error_press( matchobj, suptitle ) 1121 | plt.show() 1122 | plot_error_sw_profile(matchobj, suptitle ) 1123 | plt.show() 1124 | plot_xplot( matchobj, suptitle ) 1125 | plt.show() 1126 | plot_match_sw_profile( matchobj, suptitle ) 1127 | plt.show() 1128 | 1129 | 1130 | def plot_match_all_uss(matchobj,suptitle): 1131 | 1132 | plot_rlp_cpr( matchobj, suptitle ) 1133 | plt.show() 1134 | plot_match_dp_production( matchobj, suptitle) 1135 | plt.show() 1136 | plot_match_dp( matchobj, suptitle, draw_phase_pressures=True ) 1137 | plt.show() 1138 | plot_error_press( matchobj, suptitle ) 1139 | plt.show() 1140 | plot_match_production(matchobj, suptitle ) 1141 | plt.show() 1142 | plot_error_production( matchobj, suptitle ) 1143 | plt.show() 1144 | plt.show() 1145 | plot_xplot( matchobj, suptitle ) 1146 | plt.show() 1147 | 1148 | 1149 | # %% [markdown] 1150 | """ 1151 | # print starting values for fit parameters 1152 | """ 1153 | # %% 1154 | 1155 | #Define match parameters for kr and Pc 1156 | 1157 | params_pckr = Parameters() 1158 | 1159 | 1160 | ## copy the parameter from the LET model fit to starting experimental data table data 1161 | 1162 | for p in result_LET.params.values(): 1163 | print(p.name,p.value,p.min,p.max,p.vary) 1164 | params_pckr.add(p.name,value=p.value,min=p.min,max=p.max,vary=p.vary) 1165 | 1166 | 1167 | # Add the Pc parameters 1168 | 1169 | #AWI2 = 0.30 1170 | #AOI2 = 0.60*1.5 1171 | #CWI2 = 11.0/1000 1172 | #SWC2 = 0.08 1173 | #SWI2 = 0.13 1174 | 1175 | #same starting values as ground truth 1176 | AWI2 = AWI 1177 | AOI2 = AOI 1178 | CWI2 = CWI 1179 | SWC2 = SWC 1180 | SWI2 = SWI 1181 | 1182 | params_pckr.add('cwi', value=CWI2, min=0.2*CWI2, max=CWI2*2.0, vary=True ) 1183 | params_pckr.add('awi', value=AWI2, min=AWI2*0.5, max=AWI2*3.0, vary=True ) 1184 | params_pckr.add('aoi', value=AOI2, min=AOI2*0.5, max=AOI2*3.0, vary=True ) 1185 | params_pckr.add('ci', value=CI, min=0.0, max=CWI2*20, vary=True ) 1186 | params_pckr.add('Swi', value=SWI2, min=0.1, max=0.3, vary=True ) 1187 | 1188 | params_pckr.pretty_print() 1189 | 1190 | #fix Ew, Eo, Tw, To 1191 | params_pckr['Ew'].vary=False 1192 | params_pckr['Eo'].vary=False 1193 | params_pckr['Tw'].vary=False 1194 | params_pckr['To'].vary=False 1195 | 1196 | 1197 | 1198 | #Define match objective to match both dp and sw 1199 | #switch from oil pressure to water pressure at 46.4 hour 1200 | #equal weight to dp and sw 1201 | 1202 | uss_matchobj = USSMatchObjective1( 1203 | model=model1, 1204 | dp_weight=1, op_weight=1, sw_weight=0, 1205 | dp_data=expdataHIS, 1206 | dp_error=0.01, 1207 | op_data =expdataHIS, 1208 | op_error = 0.05, 1209 | sw_profile_times=expdataHISsattimes, 1210 | sw_profile_data =expdataHISsatprofiles, 1211 | sw_error = 0.01, 1212 | dp_switch_time=0, # switch from delta_P_o to delta_P_w at this time 1213 | ) 1214 | 1215 | 1216 | #Check that objective function works 1217 | 1218 | uss_matchobj( params_pckr ) 1219 | 1220 | 1221 | #Check starting relperm and capillary pressure model vs experimental data tables 1222 | 1223 | plot_rlp_cpr( uss_matchobj, 'Starting kr and Pc models (start table=experimental data)', params=params_pckr) 1224 | 1225 | 1226 | # %% [markdown] 1227 | """ 1228 | # Execute fit 1229 | """ 1230 | # %% 1231 | 1232 | #Excute fit 1233 | result_pckr = Minimizer(uss_matchobj, params_pckr ).least_squares(diff_step=1e-4,verbose=2) 1234 | 1235 | # write error report 1236 | print('\n\n\n') 1237 | report_fit(result_pckr) 1238 | 1239 | 1240 | # %% [markdown] 1241 | """ 1242 | # Print table with fit results 1243 | """ 1244 | # %% 1245 | 1246 | result_pckr.params.pretty_print() 1247 | 1248 | 1249 | #Evaluate objective at best point 1250 | 1251 | uss_matchobj( result_pckr.params ); 1252 | 1253 | 1254 | # %% [markdown] 1255 | """ 1256 | # Make Plots 1257 | """ 1258 | # %% 1259 | plot_match_all_uss(uss_matchobj,'USS Match dp and production') 1260 | #plot_match_all(uss_matchobj,'USS Match dp and production') 1261 | 1262 | 1263 | # %% [markdown] 1264 | """ 1265 | # Plot Saturation change along plug for each flooding period 1266 | """ 1267 | # %% 1268 | 1269 | #Saturation change along plug for each flooding period 1270 | 1271 | plt.figure(figsize=(15,4)) 1272 | 1273 | result1 = model1.results 1274 | 1275 | lw = result1.movie_sw[0,:] 1276 | for i in range(1,result1.movie_period.max()+1): 1277 | hg = result1.movie_sw[result1.movie_period==i][-1,:] 1278 | plt.fill_between( result1.xD,lw,hg,alpha=0.5) 1279 | lw = hg 1280 | 1281 | plt.ylim(0,1); 1282 | plt.xlabel('x [dimless]') 1283 | plt.ylabel('Sw [v/v]'); 1284 | 1285 | plt.title('Saturation change in each flooding period'); 1286 | plt.show() 1287 | 1288 | 1289 | # %% [markdown] 1290 | """ 1291 | # Plot match of saturation profiles 1292 | """ 1293 | # %% 1294 | 1295 | plt.figure(figsize=(15,4)) 1296 | plt.plot( result_exp.xD, Swproferr, color='gray',label='experimental data'); 1297 | plt.plot( result1.xD, result1.movie_sw.T, 'k',label='match'); 1298 | for i in range(0,len(sattimelistn)): 1299 | plt.plot( result1.xD, result1.movie_sw.T[:,sattimelistn[i]], 'r-',label='match'); 1300 | plt.xlabel('xD [dimless]') 1301 | plt.ylabel('Sw [v/v]') 1302 | plt.title('Sw profiles at requested times'); 1303 | #plt.legend() 1304 | plt.show() 1305 | 1306 | 1307 | 1308 | # %% [markdown] 1309 | """ 1310 | # Plot error ranges 1311 | """ 1312 | # %% 1313 | 1314 | # error ranges for kr 1315 | 1316 | #LETnames=['Swc', 'Sorw', 'krwe', 'kroe', 'Lw', 'Ew', 'Tw', 'Lo', 'Eo', 'To'] 1317 | #errorsignw=[-1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0] 1318 | #errorsigno=[+1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0] 1319 | 1320 | LETnames=['Swc', 'Sorw', 'krwe', 'kroe', 'Lw', 'Ew', 'Tw', 'Lo', 'Eo', 'To'] 1321 | errorsignw=[-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0] 1322 | errorsigno=[+1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0] 1323 | 1324 | 1325 | result_LET=result_pckr 1326 | result_LET.params.pretty_print() 1327 | 1328 | LETfiterror = result_LET.params.copy() 1329 | LETfiterrorupw = LETfiterror.copy() 1330 | LETfiterrordownw = LETfiterror.copy() 1331 | LETfiterrorupo = LETfiterror.copy() 1332 | LETfiterrordowno = LETfiterror.copy() 1333 | 1334 | for l in range(len(LETnames)): 1335 | 1336 | stderract = LETfiterror[LETnames[l]].stderr 1337 | if stderract > np.abs(LETfiterror[LETnames[l]].max-LETfiterror[LETnames[l]].min): 1338 | stderract = np.abs(LETfiterror[LETnames[l]].max-LETfiterror[LETnames[l]].min) 1339 | 1340 | 1341 | #if LETfiterror[LETnames[l]].value+stderract/2 > LETfiterror[LETnames[l]].max: 1342 | # stderract = 2*(LETfiterror[LETnames[l]].max-LETfiterror[LETnames[l]].value) 1343 | # 1344 | #if LETfiterror[LETnames[l]].value-stderract/2 < LETfiterror[LETnames[l]].min: 1345 | # stderract = 2*(LETfiterror[LETnames[l]].value-LETfiterror[LETnames[l]].min) 1346 | 1347 | 1348 | LETfiterrorupw[LETnames[l]].value=LETfiterror[LETnames[l]].value+errorsignw[l]*stderract/2 1349 | LETfiterrorupo[LETnames[l]].value=LETfiterror[LETnames[l]].value+errorsigno[l]*stderract/2 1350 | LETfiterrordownw[LETnames[l]].value=LETfiterror[LETnames[l]].value-errorsignw[l]*stderract/2 1351 | LETfiterrordowno[LETnames[l]].value=LETfiterror[LETnames[l]].value-errorsigno[l]*stderract/2 1352 | 1353 | if LETfiterrorupw[LETnames[l]].value > LETfiterror[LETnames[l]].max: 1354 | LETfiterrorupw[LETnames[l]].value = LETfiterror[LETnames[l]].max 1355 | 1356 | if LETfiterrorupw[LETnames[l]].value < LETfiterror[LETnames[l]].min: 1357 | LETfiterrorupw[LETnames[l]].value = LETfiterror[LETnames[l]].min 1358 | 1359 | if LETfiterrorupo[LETnames[l]].value > LETfiterror[LETnames[l]].max: 1360 | LETfiterrorupo[LETnames[l]].value = LETfiterror[LETnames[l]].max 1361 | 1362 | if LETfiterrorupo[LETnames[l]].value < LETfiterror[LETnames[l]].min: 1363 | LETfiterrorupo[LETnames[l]].value = LETfiterror[LETnames[l]].min 1364 | 1365 | if LETfiterrordownw[LETnames[l]].value > LETfiterror[LETnames[l]].max: 1366 | LETfiterrordownw[LETnames[l]].value = LETfiterror[LETnames[l]].max 1367 | 1368 | if LETfiterrordownw[LETnames[l]].value < LETfiterror[LETnames[l]].min: 1369 | LETfiterrordownw[LETnames[l]].value = LETfiterror[LETnames[l]].min 1370 | 1371 | if LETfiterrordowno[LETnames[l]].value > LETfiterror[LETnames[l]].max: 1372 | LETfiterrordowno[LETnames[l]].value = LETfiterror[LETnames[l]].max 1373 | 1374 | if LETfiterrordowno[LETnames[l]].value < LETfiterror[LETnames[l]].min: 1375 | LETfiterrordowno[LETnames[l]].value = LETfiterror[LETnames[l]].min 1376 | 1377 | 1378 | 1379 | 1380 | def params_to_rlpmodel(params ): 1381 | v = params.valuesdict() 1382 | Swc = v['Swc'] 1383 | Sorw = v['Sorw'] 1384 | krwe = v['krwe'] 1385 | kroe = v['kroe'] 1386 | Lw = v['Lw'] 1387 | Ew = v['Ew'] 1388 | Tw = v['Tw'] 1389 | Lo = v['Lo'] 1390 | Eo = v['Eo'] 1391 | To = v['To'] 1392 | rlp_model = rlplib.Rlp2PLET( Swc, Sorw, Lw, Ew, Tw, 1393 | Lo, Eo, To, krwe, kroe) 1394 | return rlp_model 1395 | 1396 | def params_to_crpmodel(params ): 1397 | v = params.valuesdict() 1398 | Swc = v['Swc'] 1399 | Sorw = v['Sorw'] 1400 | Swi = v['Swi'] 1401 | cwi = v['cwi'] 1402 | awi = v['awi'] 1403 | aoi = v['aoi'] 1404 | ci = v['ci'] 1405 | # We take Sorw for Pc to the right of the relperm Sorw 1406 | Sorwi = Sorw - 0.01 1407 | skj_model = Skjaeveland( Swc, Sorwi, Swi, cwi, awi, aoi, ci ) 1408 | # We give the Skjaevland Pc model to the simulator as a cubic interpolation model 1409 | EPS = 0.001 1410 | n = 101 1411 | swvi = np.linspace( skj_model.Swc+EPS, 1-Sorwi-EPS, n ) 1412 | pcvi = skj_model.calc(swvi) 1413 | cpr_model = rlplib.CubicInterpolator( swvi, pcvi, lex=1, rex=1 ) 1414 | return cpr_model 1415 | 1416 | 1417 | 1418 | rlp_model3w = params_to_rlpmodel(LETfiterrorupw) 1419 | rlp_model3o = params_to_rlpmodel(LETfiterrorupo) 1420 | rlp_model4w = params_to_rlpmodel(LETfiterrordownw) 1421 | rlp_model4o = params_to_rlpmodel(LETfiterrordowno) 1422 | 1423 | # pc 1424 | #crp_model3 = params_to_crpmodel(bestfitparams) 1425 | 1426 | 1427 | swv = np.linspace(0,1,101) 1428 | plt.figure() 1429 | plt.plot( swv, rlpmodel_LET_fit.calc_kr1(swv), 'b', label='krw fit') 1430 | plt.plot( swv, rlpmodel_LET_fit.calc_kr2(swv), 'r', label='kro fit') 1431 | plt.plot( swv, rlp_model3w.calc_kr1(swv), 'b', linewidth=0.5, label='+stderr') 1432 | plt.plot( swv, rlp_model3o.calc_kr2(swv), 'r', linewidth=0.5, label='+stderr') 1433 | plt.plot( swv, rlp_model4w.calc_kr1(swv), 'b', linewidth=0.5, label='-stderr') 1434 | plt.plot( swv, rlp_model4o.calc_kr2(swv), 'r', linewidth=0.5, label='-stderr') 1435 | plt.fill_between( swv, rlp_model4w.calc_kr1(swv), rlp_model3w.calc_kr1(swv), facecolor='lightblue', interpolate=True) 1436 | plt.fill_between( swv, rlp_model4o.calc_kr2(swv), rlp_model3o.calc_kr2(swv), facecolor='mistyrose', interpolate=True) 1437 | #plt.plot( Kr_data.Sat, Kr_data.kr1, 'bo') 1438 | plt.errorbar( Kr_data.Sat, Kr_data.kr1, xerr=0.05, yerr=np.abs(Kr_data.kr1*krerrlevel), fmt='o', color='blue', label='krw manual match') 1439 | #plt.plot( Kr_data.Sat, Kr_data.kr2, 'ro'); 1440 | plt.errorbar( Kr_data.Sat, Kr_data.kr2, xerr=0.05, yerr=np.abs(Kr_data.kr2*krerrlevel), fmt='o', color='red', label='kroo manual match') 1441 | plt.ylim(0,1) 1442 | #plt.legend(); 1443 | plt.xlabel('saturation Sw'); plt.ylabel('relative permeability') 1444 | plt.title('Uncertainty ranges for fitting data with $k_r$ LET model'); 1445 | plt.show() 1446 | 1447 | plt.semilogy( swv, rlpmodel_LET_fit.calc_kr1(swv), 'b', label='krw fit') 1448 | plt.semilogy( swv, rlpmodel_LET_fit.calc_kr2(swv), 'r', label='kro fit') 1449 | plt.semilogy( swv, rlp_model3w.calc_kr1(swv), 'b', linewidth=0.5, label='+stderr') 1450 | plt.semilogy( swv, rlp_model3o.calc_kr2(swv), 'r', linewidth=0.5, label='+stderr') 1451 | plt.semilogy( swv, rlp_model4w.calc_kr1(swv), 'b', linewidth=0.5, label='-stderr') 1452 | plt.semilogy( swv, rlp_model4o.calc_kr2(swv), 'r', linewidth=0.5, label='-stderr') 1453 | plt.fill_between( swv, rlp_model4w.calc_kr1(swv), rlp_model3w.calc_kr1(swv), facecolor='lightblue', interpolate=True) 1454 | plt.fill_between( swv, rlp_model4o.calc_kr2(swv), rlp_model3o.calc_kr2(swv), facecolor='mistyrose', interpolate=True) 1455 | plt.semilogy( Kr_data.Sat, Kr_data.kr1, 'bo') 1456 | #plt.errorbar( Kr_data.Sat, Kr_data.kr1, xerr=0.05, yerr=np.abs(Kr_data.kr1*0.1), fmt='o', color='blue', label='krw manual match') 1457 | plt.semilogy( Kr_data.Sat, Kr_data.kr2, 'ro'); 1458 | #plt.errorbar( Kr_data.Sat, Kr_data.kr2, xerr=0.05, yerr=np.abs(Kr_data.kr2*0.1), fmt='o', color='red', label='kroo manual match') 1459 | 1460 | plt.ylim(1E-3,1) 1461 | #plt.legend(); 1462 | plt.xlabel('saturation Sw'); plt.ylabel('relative permeability') 1463 | #plt.title('Best fit of LET relperms to tabulated kr'); 1464 | plt.show() 1465 | 1466 | 1467 | 1468 | # %% [markdown] 1469 | """ 1470 | # Plot correlation matrix 1471 | """ 1472 | # %% 1473 | 1474 | def correlation_from_covariance(covariance): 1475 | v = np.sqrt(np.diag(covariance)) 1476 | outer_v = np.outer(v, v) 1477 | correlation = covariance / outer_v 1478 | correlation[covariance == 0] = 0 1479 | return correlation 1480 | 1481 | 1482 | correlation=correlation_from_covariance(result_LET.covar) 1483 | 1484 | #LETSKnames=['Swc', 'Sorw', 'krwe', 'kroe', 'Lw', 'Ew', 'Tw', 'Lo', 'Eo', 'To','cwi','awi','aoi','ci','Swi'] 1485 | LETSKnames=['Sorw', 'krwe', 'kroe', 'Lw', 'Lo', 'cwi','awi','aoi','ci','Swi'] #Swc=fixed 1486 | 1487 | #corr = pd.DataFrame(data=correlation, index=LETnames, columns=LETnames) 1488 | corr = pd.DataFrame(data=correlation, index=LETSKnames, columns=LETSKnames) 1489 | 1490 | 1491 | # Generate a mask for the upper triangle; True = do NOT show 1492 | mask = np.zeros_like(corr, dtype=bool) 1493 | mask[np.triu_indices_from(mask)] = True 1494 | 1495 | # Set up the matplotlib figure 1496 | fig, ax = plt.subplots(figsize=(11, 9)) 1497 | 1498 | # Generate a custom diverging colormap 1499 | cmap = sns.diverging_palette(220, 10, as_cmap=True) 1500 | 1501 | # Draw the heatmap with the mask and correct aspect ratio 1502 | # More details at https://seaborn.pydata.org/generated/seaborn.heatmap.html 1503 | sns.heatmap( 1504 | corr, # The data to plot 1505 | mask=mask, # Mask some cells 1506 | cmap=cmap, # What colors to plot the heatmap as 1507 | annot=True, # Should the values be plotted in the cells? 1508 | vmax=1.0, # The maximum value of the legend. All higher vals will be same color 1509 | vmin=-1.0, # The minimum value of the legend. All lower vals will be same color 1510 | center=0, # The center value of the legend. With divergent cmap, where white is 1511 | square=True, # Force cells to be square 1512 | linewidths=.5, # Width of lines that divide cells 1513 | cbar_kws={"shrink": .5} # Extra kwargs for the legend; in this case, shrink by 50% 1514 | ) 1515 | 1516 | plt.show() 1517 | # %% 1518 | -------------------------------------------------------------------------------- /python/expdataHISSSimbibition.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/python/expdataHISSSimbibition.xlsx -------------------------------------------------------------------------------- /python/expdataHISUSSdrainage.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/python/expdataHISUSSdrainage.xlsx -------------------------------------------------------------------------------- /python/expdataHISUSSimbibition.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sede-open/Core2Relperm/a8274ef865c9a4d7d43ed25b7c8d81d79c3e2649/python/expdataHISUSSimbibition.xlsx -------------------------------------------------------------------------------- /python/scallib001/displacementmodel1D2P001.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | #-------------------------------------------------------------------------- 7 | # 8 | # 1D2P solver 9 | # - 2 phase incompressible flow with capillary pressure and gravity 10 | # - uni-directional flow ONLY, i.e., NO counter-current flow 11 | # - CPU intensive components compiled with numba 12 | # 13 | # - Assumptions for units: 14 | # * pressure in bar 15 | # * volumetric rate in cm3/minute, volume cm3 16 | # * density kg/m3 17 | # * viscosity cP 18 | # * permeability in mDarcy 19 | # * length in cm, area in cm2 20 | # * time in hour 21 | # 22 | # 03.04.2020 HD 23 | # - completed set of columns in tss_table 24 | # - included calculation of total pressure drop 25 | # i.e., assume linear relperms in block before inlet 26 | # - include PVinj as column in tss_table 27 | # - information about columns in tss_table 28 | # 29 | #-------------------------------------------------------------------------- 30 | 31 | 32 | # Constants of Nature: 33 | gravity_constant = 9.8066 # m/s**2 34 | 35 | # Conversion constants to convert to SI units in calculations: 36 | 37 | METER = 1.0 # m 38 | CENTIMETER = 0.01 # m 39 | CENTIPOISE = 0.001 # Pa.s 40 | MINUTE = 60.0 # s 41 | HOUR = 3600.0 # s 42 | KILOGRAM = 1.0 # kg 43 | BAR = 1e5 # Pa 44 | DARCY = 0.9869233e-12 # m2 45 | MILLIDARCY = DARCY/1000 46 | 47 | import numpy as np 48 | import pandas as pd 49 | import numba 50 | 51 | class dictn(dict): 52 | def __getattr__(self, name): 53 | return self[name] 54 | 55 | def __setattr__(self, name, value): 56 | self[name] = value 57 | 58 | def __delattr__(self, name): 59 | del self[name] 60 | 61 | @numba.njit 62 | def solve_1D2P_version1( 63 | N=21, 64 | cpr_model=None, 65 | rlp_model=None, 66 | cpr_multiplier=1.0, 67 | viscosity_w=1.0, 68 | viscosity_n=1.0, 69 | gvhw = 0.0, 70 | gvhn = 0.0, 71 | sw_initial=0, 72 | 73 | tD_end=0.2, 74 | follow_stops = False, 75 | t_stops = [], # user imposed stops 76 | s_stops = [], # scale factors 77 | f_stops = [], # fractional flows 78 | 79 | max_nr_iter=10, 80 | nr_tolerance=1e-3, 81 | verbose=1, 82 | 83 | max_dsw = 0.1, 84 | max_num_step=1e20, 85 | max_step_size = 0.01, 86 | start_step_size = 0.001, 87 | 88 | refine_grid = False, 89 | 90 | reporting = True, 91 | 92 | ): 93 | '''Stripped 1D2P uni-directional flow solver optimized for compilation with numba. 94 | 95 | - 2 phase incompressible flow with capillary pressure and gravity 96 | - uni-directional flow ONLY, i.e., NO counter-current flow 97 | - solver uses 1 dummy cell at inlet, will be removed from results 98 | ''' 99 | 100 | # Add dummy cell at the inlet 101 | N1 = N + 1 102 | 103 | if refine_grid: 104 | 105 | delx = 1.0 / (N1-5-1) 106 | 107 | delxi = np.zeros(N1) 108 | delxi[ 0] = 0.1 * delx # The dummy inlet cell 109 | delxi[ 1] = 0.1 * delx 110 | delxi[ 2] = 0.2 * delx 111 | delxi[ 3] = 0.4 * delx 112 | delxi[ 4] = 0.8 * delx 113 | delxi[-1] = 0.1 * delx 114 | delxi[-2] = 0.2 * delx 115 | delxi[-3] = 0.4 * delx 116 | delxi[-4] = 0.8 * delx 117 | delxi[5:-4] = delx 118 | 119 | dtr = np.zeros( N1 ) 120 | dtr[1:-1] = 2.0 / (delxi[2:]+delxi[1:-1]) 121 | dtr[ 0] = 2.0 / delxi[ 1] 122 | dtr[-1] = 2.0 / delxi[-1] 123 | 124 | else: 125 | 126 | delx = 1.0/(N1-1) # First cell is inlet, outlet cell not modelled 127 | 128 | delxi = np.ones( N1 ) * delx 129 | 130 | dtr = np.ones( N1 ) / delx 131 | dtr[ 0] = 2.0/delxi[ 0] 132 | dtr[-1] = 2.0/delxi[-1] 133 | 134 | # Mid cell position 135 | xD = np.zeros(N1) 136 | xD[0] = -0.5*delxi[0] 137 | xD[1] = 0.5*delxi[1] 138 | for i in range(2,N1): 139 | xD[i] = xD[i-1] + 0.5*(delxi[i-1]+delxi[i]) 140 | 141 | # Set initial water saturation distribution 142 | #sw = np.ones(N1,dtype=np.float64) * sw_initial trying to fix error in Python 3.11 but that is probalby not the cause 143 | sw = np.ones(N1).astype(np.float64) * sw_initial 144 | 145 | # Make snapshots at each timestep 146 | movie_tD = [] 147 | movie_sw = [] 148 | movie_dtD = [] 149 | movie_pw = [] 150 | movie_pn = [] 151 | movie_pcw = [] 152 | movie_delp = [] 153 | movie_flxw = [] 154 | movie_nr = [] 155 | 156 | # Initialize simulation time and timestep size 157 | t_stops = np.sort( t_stops ) 158 | t_stops = np.unique( t_stops ) 159 | 160 | timestep = 0 161 | dtD = start_step_size 162 | tD = 0.0 163 | 164 | for i_t_stops in range(len(t_stops)): 165 | if t_stops[i_t_stops]>tD: 166 | break 167 | 168 | dtD, i_t_stops, pres_conv, Fw = update_step( tD, dtD, tD_end, i_t_stops, t_stops, s_stops, f_stops, follow_stops ) 169 | 170 | # Pre-allocate arrays for solver 171 | jac = np.zeros((N1-1,N1-1)) 172 | 173 | pcw = np.zeros(N1) 174 | pcwd = np.zeros(N1) 175 | dpcwdx = np.zeros(N1) 176 | dpcwdxdu = np.zeros(N1) 177 | dpcwdxdd = np.zeros(N1) 178 | 179 | while tDmax_num_step: 182 | raise ValueError('FATAL max number of time steps reached - STOP') 183 | 184 | sw_prv = sw[1:].copy() 185 | 186 | sw_new = sw[1:] # note: inlet cell is not active, not solved for 187 | 188 | gvhwD = gvhw * pres_conv 189 | gvhnD = gvhn * pres_conv 190 | 191 | gvhD = gvhwD - gvhnD 192 | 193 | for iter in range(max_nr_iter): 194 | 195 | rlpw, rlpn, rlpwd, rlpnd = rlp_model.calc(sw) 196 | 197 | pcw, pcwd = cpr_model.calc(sw) 198 | 199 | mobw, mobn, mobwd, mobnd = rlpw/viscosity_w, rlpn/viscosity_n, rlpwd/viscosity_w, rlpnd/viscosity_n 200 | 201 | mobt = mobw + mobn 202 | mobtd = mobwd + mobnd 203 | 204 | fw = mobw / mobt 205 | fwd = (mobwd * mobt - mobw * mobtd) / mobt**2 206 | 207 | pcw = pcw * pres_conv * cpr_multiplier 208 | pcwd = pcwd * pres_conv * cpr_multiplier 209 | 210 | # Derivatives at cell interfaces, convention: 211 | # - du derivative to upstream cell 212 | # - dd derivative to downstream cell 213 | 214 | dpcwdx [:-1] = (pcw[+1:]-pcw[:-1]) * dtr[:-1] 215 | dpcwdxdu[:-1] = -pcwd[:-1] * dtr[:-1] 216 | dpcwdxdd[:-1] = pcwd[+1:] * dtr[:-1] 217 | 218 | # At outlet interface: no pc outsize core 219 | dpcwdx [-1] = (0-pcw [-1]) * dtr[-1] 220 | dpcwdxdu[-1] = -pcwd[-1] * dtr[-1] 221 | 222 | # flx1: viscous and gravity contribution 223 | flx1 = fw * (1 - mobn*gvhD) 224 | flx1du = fwd * (1 - mobn*gvhD) + fw * (-mobnd*gvhD) 225 | 226 | # flx2: capillary contribution 227 | flx2 = fw * mobn * dpcwdx 228 | flx2du = fwd * mobn * dpcwdx + fw * mobnd * dpcwdx + fw * mobn * dpcwdxdu 229 | flx2dd = fw * mobn * dpcwdxdd 230 | 231 | # Impose inflow flux at inlet 232 | flx1 [0] = Fw 233 | flx1du[0] = 0 234 | flx2 [0] = 0 235 | flx2du[0] = 0 236 | flx2dd[0] = 0 237 | 238 | # Total flux 239 | flxw = flx1 + flx2 240 | flxwdu = flx1du + flx2du 241 | flxwdd = flx2dd 242 | 243 | if flxw[-1]<0: 244 | # No backflow at outlet 245 | flxw [-1] = 0 246 | flxwdu [-1] = 0 247 | flxwdd [-1] = 0 248 | 249 | rhs = (sw_new - sw_prv)*delxi[1:]/dtD + flxw[1:] - flxw[:-1] 250 | 251 | nr_residual = np.linalg.norm(rhs*dtD/delxi[1:]) 252 | 253 | if verbose>1: 254 | print('iter,residual',iter,nr_residual) 255 | 256 | if nr_residual < nr_tolerance and iter>0: 257 | break 258 | 259 | diag00 = delxi[1:]/dtD + flxwdu[1: ] - flxwdd[:-1] 260 | diag10 = - flxwdu[1:-1] 261 | diag01 = + flxwdd[1:-1] 262 | 263 | np.fill_diagonal( jac, diag00 ) 264 | np.fill_diagonal( jac[1: ], diag10 ) 265 | np.fill_diagonal( jac[:,1:], diag01 ) 266 | 267 | d_sw = np.linalg.solve( jac, rhs ) 268 | 269 | cutback_factor = np.maximum( np.abs(d_sw).max()/max_dsw, 1.0 ) 270 | 271 | sw_new -= d_sw/cutback_factor 272 | 273 | 274 | if nr_residual < nr_tolerance: 275 | 276 | # Converged, accept timestep 277 | 278 | tD = tD + dtD 279 | timestep += 1 280 | 281 | if reporting: 282 | 283 | dpcwdx[0] = (pcw[1]-0.0)*dtr[0] 284 | 285 | Sw_inlet = calc_inlet_Sw( Fw, dpcwdx[0], viscosity_w, viscosity_n, gvhwD, gvhnD )*1.0 286 | 287 | mobw[0] = ( Sw_inlet)/viscosity_w 288 | mobn[0] = (1.0-Sw_inlet)/viscosity_n 289 | mobt[0] = mobw[0] + mobn[0] 290 | 291 | delp = -(1 + dpcwdx * mobn + gvhnD * mobn + gvhwD * mobw) / dtr / mobt / pres_conv 292 | 293 | pcw /= pres_conv 294 | 295 | pw = np.zeros( sw.size ) 296 | pw[2:] = np.cumsum( delp[1:-1] ) 297 | pw -= pw[-1] - 1.0 + delp[-1] # outer edge at 1 bar 298 | 299 | pn = pw + pcw 300 | 301 | # Only report cells inside core 302 | movie_tD .append(tD ) 303 | movie_dtD .append(dtD ) 304 | movie_sw .append(sw [1:].copy()) 305 | movie_delp .append(delp[0:].copy()) 306 | movie_pw .append(pw [1:].copy()) 307 | movie_pn .append(pn [1:].copy()) 308 | movie_pcw .append(pcw [1:].copy()) 309 | movie_flxw .append(flxw .copy()) 310 | 311 | movie_nr.append( iter ) 312 | 313 | if verbose>0: 314 | print (timestep,'tD, residual',tD,nr_residual,'dtD',dtD,'nr_iter',iter) 315 | 316 | 317 | dtD = np.minimum( 2.0*dtD, max_step_size ) #TODO step increment factor 318 | 319 | if tD0: 331 | print ('tD, residual,backup',tD,nr_residual,dtD) 332 | 333 | dtD = dtD / 2.0 # TODO step cutback factor 334 | 335 | # Drop dummy inlet cell 336 | xD = xD[1:] 337 | 338 | return (xD, movie_tD, movie_dtD, movie_sw, movie_pw, movie_pn, movie_pcw, movie_delp, movie_flxw, movie_nr) 339 | 340 | #TODO simplify and clean up 341 | 342 | @numba.njit 343 | def update_step( t, delt, t_end, i_t_stops, t_stops, s_stops, f_stops, follow_stops ): 344 | 345 | assert len(t_stops)>1 346 | 347 | fw = 1 348 | 349 | n = len(t_stops) 350 | 351 | if i_t_stops < n: 352 | if follow_stops: 353 | delt= t_stops[i_t_stops] - t 354 | if np.abs(delt) < 1e-6: 355 | if i_t_stops= t_stops[i_t_stops]: 370 | delt = t_stops[i_t_stops] - t 371 | pres_conv = s_stops[i_t_stops-1] 372 | fw = f_stops[i_t_stops-1] 373 | if len(t_stops)<100: print (t, t_stops[i_t_stops], delt, i_t_stops) 374 | if delt<1e-19: raise ValueError('del_t tiny 2') 375 | i_t_stops += 1 # FIXME this goes wrong if there is a cutback 376 | else: 377 | pres_conv = s_stops[-1] 378 | 379 | return delt, i_t_stops, pres_conv, fw 380 | 381 | @numba.njit 382 | def calc_inlet_Sw( FW, dPcdxD, vscw, vscn, gvhwD, gvhnD ): 383 | '''Calculate Sw at inlet set assuming linear relperms at inlet''' 384 | qw = vscw * FW 385 | qn = vscn * (1-FW) 386 | g = dPcdxD + gvhnD -gvhwD 387 | 388 | qtg = qw + qn + g 389 | 390 | q = 0.5*(qtg+np.sign(qtg)*np.sqrt( qtg**2 - 4.0*qw*g )) 391 | 392 | q = np.float64(q) 393 | Sw = 0.0 394 | if qtg>0: 395 | Sw = qw/q 396 | else: 397 | Sw = q/g 398 | 399 | return Sw 400 | 401 | class DisplacementModel1D2P(object): 402 | 403 | def __init__(self, 404 | core_length=None, 405 | core_area=None, 406 | permeability=None, 407 | porosity=None, 408 | sw_initial=None, 409 | viscosity_w=None, 410 | viscosity_n=None, 411 | density_w=None, 412 | density_n=None, 413 | gravity_multiplier=1.0, 414 | cpr_multiplier=1.0, 415 | time_end=None, 416 | rlp_model=None, 417 | cpr_model=None, 418 | rate_schedule=None, 419 | movie_schedule=None, 420 | NX=50, 421 | verbose=False, 422 | max_step_size=1.0, 423 | start_step_size=0.001, 424 | refine_grid=True, 425 | max_nr_iter=25, 426 | nr_tolerance=1e-10, 427 | ): 428 | 429 | self.core_length = core_length 430 | self.core_area = core_area 431 | self.permeability = permeability 432 | self.porosity = porosity 433 | self.sw_initial = sw_initial 434 | self.viscosity_w = viscosity_w 435 | self.viscosity_n = viscosity_n 436 | self.density_w = density_w 437 | self.density_n = density_n 438 | 439 | self.gravity_multiplier = gravity_multiplier 440 | self.cpr_multiplier = cpr_multiplier 441 | 442 | self.rlp_model = rlp_model 443 | self.cpr_model = cpr_model 444 | 445 | self.time_end = time_end 446 | self.rate_schedule = rate_schedule 447 | self.movie_schedule = movie_schedule 448 | 449 | self.NX = NX 450 | self.verbose = verbose 451 | self.max_step_size = max_step_size 452 | self.start_step_size = start_step_size 453 | self.refine_grid = refine_grid 454 | self.max_nr_iter = max_nr_iter 455 | self.nr_tolerance = nr_tolerance 456 | 457 | self.check_input_valid() 458 | 459 | self.prepare() 460 | 461 | def check_input_valid(self): 462 | 463 | # Check that all parameters have value 464 | invalid = [] 465 | for k,v in self.__dict__.items(): 466 | if v is None: 467 | invalid.append(k) 468 | if len(invalid): 469 | raise ValueError('FATAL these input parameters have no value: \n'+ '\n'.join(invalid) ) 470 | 471 | assert 'StartTime' in self.rate_schedule.columns 472 | assert 'InjRate' in self.rate_schedule.columns 473 | 474 | def prepare(self): 475 | 476 | self.pore_volume = self.core_length * self.core_area * self.porosity 477 | 478 | sim_schedule = self.prepare_sim_schedule( self.movie_schedule ) 479 | 480 | self.sim_schedule = sim_schedule 481 | 482 | def prepare_sim_schedule( self, time_stops): 483 | 484 | assert np.all( self.rate_schedule.InjRate.values>0 ), 'Injection rate must be non-zero' 485 | 486 | K = self.permeability * MILLIDARCY 487 | L = self.core_length * CENTIMETER 488 | A = self.core_area * CENTIMETER**2 489 | por = self.porosity 490 | 491 | tD_conv = HOUR * CENTIMETER**3/MINUTE/ (L*A*por) 492 | 493 | pres_conv = CENTIMETER**3/MINUTE / A * CENTIPOISE * L / K / BAR 494 | 495 | period_time = self.rate_schedule.StartTime.values 496 | period_rate = self.rate_schedule.InjRate.values 497 | 498 | sel = period_time < self.time_end 499 | period_time = np.hstack( [period_time[sel], [self.time_end ]] ) 500 | period_rate = np.hstack( [period_rate[sel], [period_rate[sel][-1]]] ) 501 | 502 | time_stops = np.array( time_stops ) 503 | 504 | time_stops = time_stops[ (time_stops>period_time.min()) & (time_stops 0 36 | 37 | w1 = w1[condition] 38 | w2 = w2[condition] 39 | m1 = m1[condition] 40 | m2 = m2[condition] 41 | 42 | dk = np.zeros_like(y) 43 | dk[1:-1][condition] = m1*m2*(w1+w2)/(w1*m2+w2*m1) 44 | 45 | if lex==0: 46 | # Truncation 47 | dk[ 0] = 0 48 | else: 49 | # Linear extrapolation 50 | dk[ 0] = mk[ 0 ] 51 | 52 | if rex==0: 53 | # Truncation 54 | dk[-1] = 0 55 | else: 56 | # Linear extrapolation 57 | dk[-1] = mk[-1] 58 | 59 | return dk 60 | 61 | @numba.njit 62 | def pchip_fit( xi, yi, lex=1, rex=1 ): 63 | '''Fit coefficients for piecewise cubic hermite spline function.''' 64 | 65 | # Extend interval to enforce truncation or linear extrapolation 66 | 67 | n = len(xi) 68 | 69 | x0 = np.zeros(n+2) 70 | y0 = np.zeros(n+2) 71 | 72 | x0[ 0] = xi[ 0] - (xi[ 1] - xi[ 0]) 73 | x0[-1] = xi[-1] + (xi[-1] - xi[-2]) 74 | 75 | x0[1:-1] = xi 76 | 77 | if lex==0: 78 | y0[ 0] = yi[0] 79 | else: 80 | y0[0] = yi[0] - (yi[ 1] - yi[ 0]) 81 | 82 | if rex==0: 83 | y0[-1] = yi[-1] 84 | else: 85 | y0[-1] = yi[-1] + (yi[-1] - yi[-2]) 86 | 87 | y0[1:-1] = yi 88 | 89 | # Estimate derivatives 90 | d0 = pchip_estimate_derivatives( x0, y0, lex, rex ) 91 | 92 | # Return extended interval and coefficients 93 | return x0, pchip_coefs( x0, y0, d0 ) 94 | 95 | @numba.njit 96 | def pchip_coefs( xi, yi, di ): 97 | '''Calculate coefficients for piecewise cubic hermite spline function.''' 98 | x1 = xi[:-1] 99 | x2 = xi[+1:] 100 | 101 | f1 = yi[:-1] 102 | f2 = yi[+1:] 103 | 104 | d1 = di[:-1] 105 | d2 = di[+1:] 106 | 107 | h = x2 - x1 108 | df = ( f2 - f1 ) / h 109 | 110 | c0 = f1 111 | c1 = d1 112 | c2 = - ( 2.0 * d1 - 3.0 * df + d2 ) / h 113 | c3 = ( d1 - 2.0 * df + d2 ) / h / h 114 | 115 | return c0,c1,c2,c3 116 | 117 | @numba.njit 118 | def pchip_eval( x, xi, c0, c1, c2, c3 ): 119 | '''Evalulate piecewise cubic hermite spline function and its derivative at x.''' 120 | left = np.digitize( x, xi )-1 121 | left = np.maximum( left, 0 ) 122 | left = np.minimum( left, len(xi)-2 ) 123 | 124 | dx = x - xi[left] 125 | 126 | c0 = c0[left] 127 | c1 = c1[left] 128 | c2 = c2[left] 129 | c3 = c3[left] 130 | 131 | f = c0 + dx* ( c1 + dx * (c2 + dx*c3 ) ) 132 | 133 | d = c1 + dx * ( 2.0 * c2 + dx * 3.0 * c3 ) 134 | 135 | return f, d 136 | 137 | def make_pchip( xi, yi, lex=1, rex=1 ): 138 | '''Create piecewise cubic hermite spline evaluation function.''' 139 | lex = lex 140 | rex = rex 141 | 142 | x0,(c0,c1,c2,c3) = pchip_fit( xi, yi, lex, rex ) 143 | 144 | @numba.njit 145 | def itp(x): 146 | return pchip_eval( x, x0, c0, c1, c2, c3 ) 147 | 148 | # compile 149 | itp(np.ones(2)) 150 | 151 | return itp 152 | 153 | spec = [ 154 | ('xi', numba.float64[:] ), 155 | ('yi', numba.float64[:] ), 156 | ('lex', numba.int64 ), 157 | ('rex', numba.int64 ), 158 | ('x0', numba.float64[:] ), 159 | ('c0', numba.float64[:] ), 160 | ('c1', numba.float64[:] ), 161 | ('c2', numba.float64[:] ), 162 | ('c3', numba.float64[:] ), 163 | ] 164 | 165 | @numba.experimental.jitclass(spec) 166 | class PchipInterpolator(object): 167 | '''Piecewise cubic hermite spline interpolator.''' 168 | def __init__(self,xi,yi,lex=1,rex=1): 169 | self.xi = xi 170 | self.yi = yi 171 | self.lex = lex 172 | self.rex = rex 173 | 174 | x0,(c0,c1,c2,c3) = pchip_fit( xi, yi, lex, rex ) 175 | 176 | self.x0 = x0 177 | self.c0 = c0 178 | self.c1 = c1 179 | self.c2 = c2 180 | self.c3 = c3 181 | 182 | def calc(self,x): 183 | return pchip_eval( x, self.x0, self.c0, self.c1, self.c2, self.c3 ) 184 | 185 | make_cubic_interpolator = make_pchip 186 | 187 | CubicInterpolator = PchipInterpolator 188 | 189 | #------------------------------------------------------------------------------------- 190 | #--- Linear & cubic interpolation of tabular data 191 | # Corey and LET functions 192 | #------------------------------------------------------------------------------------- 193 | 194 | @numba.njit 195 | def rlp_2p_linear(sat1v,sat1_data,kr1_data,kr2_data,lex=0,rex=0): 196 | 197 | i = np.digitize( sat1v, sat1_data )-1 198 | 199 | i = np.minimum( i, len(sat1_data)-2 ) 200 | i = np.maximum( i, 0 ) 201 | 202 | xl = sat1_data[i] 203 | xr = sat1_data[i+1] 204 | 205 | yl = kr1_data[i] 206 | yr = kr1_data[i+1] 207 | 208 | k1d = (yr-yl)/(xr-xl) 209 | k1 = yl + k1d*(sat1v-xl) 210 | 211 | yl = kr2_data[i] 212 | yr = kr2_data[i+1] 213 | 214 | k2d = (yr-yl)/(xr-xl) 215 | k2 = yl + k2d*(sat1v-xl) 216 | 217 | if lex==0: 218 | # Truncate left 219 | l = sat1vsat1_data[-1] 230 | 231 | k1[r] = kr1_data[-1] 232 | k2[r] = kr2_data[-1] 233 | 234 | k1d[r] = 0 235 | k2d[r] = 0 236 | 237 | return k1, k2, k1d, k2d 238 | 239 | @numba.njit 240 | def power_eps(s, N, eps): 241 | """Power function and its derivative; when N<1 a regularized version is used 242 | to prevent infinite derivative values at endpoints. 243 | 244 | When N >= 1: 245 | returns s**N and N s**(N-1) 246 | 247 | When N < 1: 248 | returns ((s + eps)**N - eps**N)/((1 + eps)**N - eps**N) and 249 | N (s + eps)**(N-1)/((1 + eps)**N - eps**N) 250 | 251 | When N < 1e-4 the limiting value valid for N=0 is returned 252 | returns log(1 + s/eps)/log(1 + 1/eps) and 253 | 1/(1+s/eps) / eps / log(1 + 1/eps) 254 | 255 | Note: 256 | the power function is valid only for s >= 0 and N >= 0 257 | both s and N are clipped to 0 when values are negative. 258 | """ 259 | 260 | # Make sure s is non-negative 261 | s = np.maximum(s, 0.0) 262 | 263 | if N < 1.0 and eps > 0.0: 264 | 265 | # Regularized power function 266 | 267 | # Write code such that minimum number of power function calls is used (2) 268 | 269 | epss = 1.0 + s/eps 270 | eps1 = 1.0 + 1.0/eps 271 | 272 | if N > 1e-4: 273 | 274 | epssN = np.power(epss, N) 275 | eps1N = np.power(eps1, N) 276 | 277 | epssNmin1 = epssN / epss 278 | 279 | dinv = 1.0/(eps1N - 1.0) 280 | 281 | f = (epssN - 1.0) * dinv 282 | fd = N * epssNmin1 / eps * dinv 283 | 284 | else: 285 | 286 | # Limiting values when N = 0 287 | 288 | logepss = np.log(epss) 289 | logeps1 = np.log(eps1) 290 | 291 | dinv = 1.0/logeps1 292 | 293 | f = logepss * dinv 294 | fd = 1.0/epss / eps * dinv 295 | 296 | else: 297 | 298 | # Standard power function 299 | 300 | f = np.power(s, N) 301 | fd = N * np.power(s, N-1.0) 302 | 303 | return f, fd 304 | 305 | 306 | @numba.njit 307 | def rlp_2p_corey(sat1v,Sr1,Sr2,N1,N2,Ke1,Ke2,eps=1e-4): 308 | 309 | n = len(sat1v) 310 | 311 | kr1 = np.zeros( n ) 312 | kr2 = np.zeros( n ) 313 | 314 | kr1d = np.zeros( n ) 315 | kr2d = np.zeros( n ) 316 | 317 | for i in range(n): 318 | 319 | s1 = sat1v[i] 320 | 321 | if s11-Sr2: 324 | kr1[i] = Ke1 325 | else: 326 | ss = (s1-Sr1)/(1-Sr2-Sr1) 327 | 328 | f1, f1d = power_eps(ss, N1, eps) 329 | f2, f2d = power_eps(1.0-ss, N2, eps) 330 | 331 | kr1[i] = f1 * Ke1 332 | kr2[i] = f2 * Ke2 333 | 334 | kr1d[i] = +f1d * Ke1 / (1-Sr1-Sr2) 335 | kr2d[i] = -f2d * Ke2 / (1-Sr1-Sr2) 336 | 337 | return kr1, kr2, kr1d, kr2d 338 | 339 | @numba.njit 340 | def rlp_2p_let(sat1v,Sr1,Sr2,L1,E1,T1,L2,E2,T2,Ke1,Ke2,eps=1e-4): 341 | 342 | n = len(sat1v) 343 | 344 | kr1 = np.zeros( n ) 345 | kr2 = np.zeros( n ) 346 | 347 | kr1d = np.zeros( n ) 348 | kr2d = np.zeros( n ) 349 | 350 | Ke1s = Ke1 / (1-Sr1-Sr2) 351 | Ke2s = Ke2 / (1-Sr1-Sr2) 352 | 353 | for i in range(n): 354 | 355 | s1 = sat1v[i] 356 | 357 | if s11-Sr2: 360 | kr1[i] = Ke1 361 | else: 362 | ss = (s1-Sr1)/(1-Sr2-Sr1) 363 | 364 | pw0f, pw0d = power_eps(ss, L1, eps) 365 | po0f, po0d = power_eps(1-ss, L2, eps) 366 | 367 | if E1>0: 368 | 369 | pw1f, pw1d = power_eps(1-ss, T1, eps) 370 | 371 | dw = (pw0f+E1*pw1f) 372 | 373 | kr1 [i] = Ke1 * pw0f/dw 374 | kr1d[i] = Ke1s * (pw0d*pw1f + pw0f*pw1d) / (dw*dw) * E1 375 | 376 | else: 377 | 378 | kr1 [i] = Ke1 * pw0f 379 | kr1d[i] = Ke1s * pw0d 380 | 381 | if E2>0: 382 | 383 | po1f, po1d = power_eps(ss, T2, eps) 384 | 385 | do = (po0f+E2*po1f) 386 | 387 | kr2 [i] = Ke2 * po0f/do 388 | kr2d[i] = -Ke2s * (po0d*po1f + po0f*po1d) / (do*do) * E2 389 | 390 | else: 391 | 392 | kr2 [i] = Ke2 * po0f 393 | kr2d[i] = -Ke2s * po0d 394 | 395 | return kr1, kr2, kr1d, kr2d 396 | 397 | spec = [ 398 | ('sat1', numba.float64[:] ), 399 | ('kr1', numba.float64[:] ), 400 | ('kr2', numba.float64[:] ), 401 | ('lex', numba.int64 ), 402 | ('rex', numba.int64 ), 403 | ] 404 | 405 | @numba.experimental.jitclass(spec) 406 | class Rlp2PLinear(object): 407 | 408 | def __init__(self,sat1,kr1,kr2,lex=0,rex=0): 409 | 410 | assert len(sat1)>1 411 | assert len(kr1)>1 412 | assert len(kr2)>1 413 | 414 | # It is computationally more efficient to implement 415 | # truncation by extending input dataset with edge values 416 | 417 | if lex==0: 418 | satn = sat1[0] - (sat1[1]-sat1[0]) 419 | 420 | sat1 = np.hstack( ( np.array([satn ]), sat1 ) ) 421 | kr1 = np.hstack( ( np.array([kr1[0]]), kr1 ) ) 422 | kr2 = np.hstack( ( np.array([kr2[0]]), kr2 ) ) 423 | 424 | if rex==0: 425 | satn = sat1[-1] + (sat1[-1]-sat1[-2]) 426 | 427 | sat1 = np.hstack( ( sat1, np.array([satn ]) ) ) 428 | kr1 = np.hstack( ( kr1, np.array([kr1[-1]]) ) ) 429 | kr2 = np.hstack( ( kr2, np.array([kr2[-1]]) ) ) 430 | 431 | self.sat1 = sat1 432 | self.kr1 = kr1 433 | self.kr2 = kr2 434 | self.lex = lex 435 | self.rex = rex 436 | 437 | def calc(self,sat1v): 438 | return rlp_2p_linear( sat1v, self.sat1, self.kr1, self.kr2, 439 | lex=1, rex=1 ) 440 | 441 | def calc_kr1(self,sat1v): 442 | return self.calc(sat1v)[0] 443 | 444 | def calc_kr2(self,sat1v): 445 | return self.calc(sat1v)[1] 446 | 447 | def calc_kr1_der(self,sat1v): 448 | return self.calc(sat1v)[2] 449 | 450 | def calc_kr2_der(self,sat1v): 451 | return self.calc(sat1v)[3] 452 | 453 | spec = [ 454 | ('sat1', numba.float64[:] ), 455 | ('kr1', numba.float64[:] ), 456 | ('kr2', numba.float64[:] ), 457 | ('lex', numba.int64 ), 458 | ('rex', numba.int64 ), 459 | ('w_x0', numba.float64[:] ), 460 | ('w_c0', numba.float64[:] ), 461 | ('w_c1', numba.float64[:] ), 462 | ('w_c2', numba.float64[:] ), 463 | ('w_c3', numba.float64[:] ), 464 | ('o_x0', numba.float64[:] ), 465 | ('o_c0', numba.float64[:] ), 466 | ('o_c1', numba.float64[:] ), 467 | ('o_c2', numba.float64[:] ), 468 | ('o_c3', numba.float64[:] ), 469 | ] 470 | 471 | @numba.experimental.jitclass(spec) 472 | class Rlp2PCubic(object): 473 | 474 | def __init__(self,sat1,kr1,kr2,lex=1,rex=1): 475 | self.sat1 = sat1 476 | self.kr1 = kr1 477 | self.kr2 = kr2 478 | self.lex = lex 479 | self.rex = rex 480 | 481 | x0,(c0,c1,c2,c3) = pchip_fit( sat1, kr1, lex, rex ) 482 | 483 | self.w_x0 = x0 484 | self.w_c0 = c0 485 | self.w_c1 = c1 486 | self.w_c2 = c2 487 | self.w_c3 = c3 488 | 489 | x0,(c0,c1,c2,c3) = pchip_fit( sat1, kr2, lex, rex ) 490 | 491 | self.o_x0 = x0 492 | self.o_c0 = c0 493 | self.o_c1 = c1 494 | self.o_c2 = c2 495 | self.o_c3 = c3 496 | 497 | def calc(self,sat1v): 498 | k1,k1d = pchip_eval( sat1v, self.w_x0, self.w_c0, self.w_c1, self.w_c2, self.w_c3 ) 499 | k2,k2d = pchip_eval( sat1v, self.o_x0, self.o_c0, self.o_c1, self.o_c2, self.o_c3 ) 500 | return k1,k2,k1d,k2d 501 | 502 | def calc_kr1(self,sat1v): 503 | return self.calc(sat1v)[0] 504 | 505 | def calc_kr2(self,sat1v): 506 | return self.calc(sat1v)[1] 507 | 508 | def calc_kr1_der(self,sat1v): 509 | return self.calc(sat1v)[2] 510 | 511 | def calc_kr2_der(self,sat1v): 512 | return self.calc(sat1v)[3] 513 | 514 | spec = [ 515 | ('Sr1', numba.float64 ), 516 | ('Sr2', numba.float64 ), 517 | ('L1', numba.float64 ), 518 | ('E1', numba.float64 ), 519 | ('T1', numba.float64 ), 520 | ('L2', numba.float64 ), 521 | ('E2', numba.float64 ), 522 | ('T2', numba.float64 ), 523 | ('Ke1', numba.float64 ), 524 | ('Ke2', numba.float64 ), 525 | ('eps', numba.float64 ), 526 | ] 527 | 528 | @numba.experimental.jitclass(spec) 529 | class Rlp2PLET(object): 530 | 531 | def __init__(self,Sr1,Sr2,L1,E1,T1,L2,E2,T2,Ke1,Ke2,eps=1e-4): 532 | 533 | if Sr1 < 0.0: 534 | raise ValueError("Sr1 is negative, choose 0<=Sr1<1") 535 | if Sr2 < 0.0: 536 | raise ValueError("Sr2 is negative, choose 0<=Sr2<1") 537 | if Sr1 >= 1.0: 538 | raise ValueError("Sr1 > 1, choose 0<=Sr1<1") 539 | if Sr2 >= 1.0: 540 | raise ValueError("Sr2 > 1, choose 0<=Sr2<1") 541 | if 1-Sr1-Sr2 < 1e-6: 542 | raise ValueError("1-Sr1-Sr2 is not positive, adjust Sr1 and/or Sr2") 543 | if L1 <= 0.0: 544 | raise ValueError("L1 is non-positive, choose L1 > 0") 545 | if L2 <= 0.0: 546 | raise ValueError("L2 is non-positive, choose L2 > 0") 547 | if E1 < 0.0: 548 | raise ValueError("E1 is negative, choose E1 >= 0") 549 | if E2 < 0.0: 550 | raise ValueError("E2 is negative, choose E2 >= 0") 551 | if E1>0.0 and T1 <= 0.0: 552 | raise ValueError("T1 is non-positive, choose T1 > 0") 553 | if E2>0.0 and T2 <= 0.0: 554 | raise ValueError("T2 is non-positive, choose T2 > 0") 555 | if Ke1 <= 0.0: 556 | raise ValueError("Ke1 is non-positive, choose Ke1 > 0") 557 | if Ke2 <= 0.0: 558 | raise ValueError("Ke2 is non-positive, choose Ke2 > 0") 559 | if eps < 0.0: 560 | raise ValueError("eps is negative, choose eps >= 0") 561 | if L1 < 1.0 and eps == 0: 562 | raise ValueError("eps must be > 0 when L1 < 1, otherwise infinite derivative at endpoint") 563 | if L2 < 1.0 and eps == 0: 564 | raise ValueError("eps must be > 0 when L2 < 1, otherwise infinite derivative at endpoint") 565 | if E1>0.0 and T1 < 1.0 and eps == 0: 566 | raise ValueError("eps must be > 0 when T1 < 1, otherwise infinite derivative at endpoint") 567 | if E2>0.0 and T2 < 1.0 and eps == 0: 568 | raise ValueError("eps must be > 0 when T2 < 1, otherwise infinite derivative at endpoint") 569 | 570 | self.Sr1 = Sr1 571 | self.Sr2 = Sr2 572 | self.L1 = L1 573 | self.E1 = E1 574 | self.T1 = T1 575 | self.L2 = L2 576 | self.E2 = E2 577 | self.T2 = T2 578 | self.Ke1 = Ke1 579 | self.Ke2 = Ke2 580 | self.eps = eps 581 | 582 | def calc(self,sat1v): 583 | return rlp_2p_let(sat1v,self.Sr1,self.Sr2, 584 | self.L1,self.E1,self.T1, 585 | self.L2, self.E2,self.T2, 586 | self.Ke1,self.Ke2, self.eps) 587 | 588 | def calc_kr1(self,sat1v): 589 | return self.calc(sat1v)[0] 590 | 591 | def calc_kr2(self,sat1v): 592 | return self.calc(sat1v)[1] 593 | 594 | def calc_kr1_der(self,sat1v): 595 | return self.calc(sat1v)[2] 596 | 597 | def calc_kr2_der(self,sat1v): 598 | return self.calc(sat1v)[3] 599 | 600 | spec = [ 601 | ('Sr1', numba.float64 ), 602 | ('Sr2', numba.float64 ), 603 | ('N1', numba.float64 ), 604 | ('N2', numba.float64 ), 605 | ('Ke1', numba.float64 ), 606 | ('Ke2', numba.float64 ), 607 | ('eps', numba.float64 ), 608 | ] 609 | 610 | @numba.experimental.jitclass(spec) 611 | class Rlp2PCorey(object): 612 | 613 | def __init__(self,Sr1,Sr2,N1,N2,Ke1,Ke2,eps=1e-4): 614 | 615 | if Sr1 < 0.0: 616 | raise ValueError("Sr1 is negative, choose 0<=Sr1<1") 617 | if Sr2 < 0.0: 618 | raise ValueError("Sr2 is negative, choose 0<=Sr2<1") 619 | if Sr1 >= 1.0: 620 | raise ValueError("Sr1 > 1, choose 0<=Sr1<1") 621 | if Sr2 >= 1.0: 622 | raise ValueError("Sr2 > 1, choose 0<=Sr2<1") 623 | if 1-Sr1-Sr2 < 1e-6: 624 | raise ValueError("1-Sr1-Sr2 is negative, adjust Sr1 and/or Sr2") 625 | if N1 <= 0.0: 626 | raise ValueError("N1 is non-positive, choose N1 > 0") 627 | if N2 <= 0.0: 628 | raise ValueError("N2 is non-positive, choose N2 > 0") 629 | if Ke1 <= 0.0: 630 | raise ValueError("Ke1 is non-positive, choose Ke1 > 0") 631 | if Ke2 <= 0.0: 632 | raise ValueError("Ke2 is non-positive, choose Ke2 > 0") 633 | if eps < 0.0: 634 | raise ValueError("eps is negative, choose eps >= 0") 635 | if N1 < 1.0 and eps == 0: 636 | raise ValueError("eps must be > 0 when N1 < 1, otherwise infinite derivative at endpoint") 637 | if N2 < 1.0 and eps == 0: 638 | raise ValueError("eps must be > 0 when N2 < 1, otherwise infinite derivative at endpoint") 639 | 640 | self.Sr1 = Sr1 641 | self.Sr2 = Sr2 642 | self.N1 = N1 643 | self.N2 = N2 644 | self.Ke1 = Ke1 645 | self.Ke2 = Ke2 646 | self.eps = eps 647 | 648 | def calc(self,sat1v): 649 | return rlp_2p_corey(sat1v,self.Sr1,self.Sr2, 650 | self.N1,self.N2,self.Ke1,self.Ke2, 651 | self.eps) 652 | 653 | def calc_kr1(self,sat1v): 654 | return self.calc(sat1v)[0] 655 | 656 | def calc_kr2(self,sat1v): 657 | return self.calc(sat1v)[1] 658 | 659 | def calc_kr1_der(self,sat1v): 660 | return self.calc(sat1v)[2] 661 | 662 | def calc_kr2_der(self,sat1v): 663 | return self.calc(sat1v)[3] 664 | -------------------------------------------------------------------------------- /python/scallib001/tests/README.md: -------------------------------------------------------------------------------- 1 | ## Collection of functionality and cpu performance tests for scallib001 2 | 3 | The tests make use of the standard python test library [pytest](https://docs.pytest.org/en/7.1.x/getting-started.html). 4 | 5 | When necessary, pytest can be installed using pip: pip install pytest. 6 | 7 | If in addition you want to do cpu performance benchmarking, you need to 8 | install the pytest plugin [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/). 9 | 10 | The pytest-benchmark plugin for pytest can be installed using pip: pip install pytest-benchmark. 11 | 12 | To run the tests, go to the directory that contains scallib001 and do 13 | 14 | python -m pytest scallib001 15 | 16 | -------------------------------------------------------------------------------- /python/scallib001/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import pytest 7 | 8 | try: 9 | import pytest_benchmark 10 | except: 11 | # pytest_benchmark does not installed, skip it 12 | @pytest.fixture 13 | def benchmark(*args, **kwargs): 14 | return None 15 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_power_eps1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import numpy as np 7 | import scallib001.relpermlib001 as rlplib 8 | 9 | 10 | def test_power_eps1(): 11 | 12 | eps = 1e-4 13 | 14 | # check power_eps(0.0, 0.0, eps), i.e., zero power at zero 15 | 16 | exact_fd_check1 = 1/eps / np.log(1 + 1/eps) 17 | 18 | assert np.allclose( rlplib.power_eps(0.0, 0.0, eps), [0.0, exact_fd_check1] ) 19 | assert np.allclose( rlplib.power_eps(0.0, 1e-10, eps), [0.0, exact_fd_check1] ) 20 | 21 | # check power_eps(x, 0.0, eps), i.e., zero power at zero and finite x 22 | 23 | sv2 = np.logspace(-6, 0, 51) 24 | 25 | exact_f_check2 = np.log(1.0 + sv2/eps)/np.log(1.0 + 1/eps) 26 | exact_fd_check2 = 1/(1.0 + sv2/eps)/eps/np.log(1.0 + 1/eps) 27 | 28 | f2, fd2 = rlplib.power_eps(sv2, 0.0, eps) 29 | 30 | assert np.allclose(f2, exact_f_check2) 31 | assert np.allclose(fd2, exact_fd_check2) 32 | 33 | 34 | 35 | def test_power_eps2(): 36 | 37 | eps = 1e-6 38 | 39 | sv1 = np.logspace(-6, 0, 51) 40 | 41 | # check that numba compiled version equals python version for N = 0 42 | 43 | numba_f1, numba_fd1 = rlplib.power_eps( sv1, 0.0, eps) 44 | python_f1, python_fd1 = rlplib.power_eps.py_func( sv1, 0.0, eps) 45 | 46 | assert np.allclose(numba_f1, python_f1) 47 | assert np.allclose(numba_fd1, python_fd1) 48 | 49 | # check that numba compiled version equals python version for N = 0.3 50 | 51 | numba_f2, numba_fd2 = rlplib.power_eps( sv1, 0.3, eps) 52 | python_f2, python_fd2 = rlplib.power_eps.py_func( sv1, 0.3, eps) 53 | 54 | assert np.allclose(numba_f2, python_f2) 55 | assert np.allclose(numba_fd2, python_fd2) 56 | 57 | # check that numba compiled version equals python version for N = 1.0 58 | 59 | numba_f3, numba_fd3 = rlplib.power_eps( sv1, 1.0, eps) 60 | python_f3, python_fd3 = rlplib.power_eps.py_func( sv1, 1.0, eps) 61 | 62 | assert np.allclose(numba_f3, python_f3) 63 | assert np.allclose(numba_fd3, python_fd3) 64 | 65 | # check that numba compiled version equals python version for N = 3.1 66 | 67 | numba_f4, numba_fd4 = rlplib.power_eps( sv1, 3.1, eps) 68 | python_f4, python_fd4 = rlplib.power_eps.py_func( sv1, 3.1, eps) 69 | 70 | assert np.allclose(numba_f4, python_f4) 71 | assert np.allclose(numba_fd4, python_fd4) 72 | 73 | def test_power_eps3(): 74 | 75 | # Take a very small value for eps 76 | eps = 1e-12 77 | 78 | sv1 = np.logspace(-6, 0, 51) 79 | 80 | # check that numba compiled version equals python version for N = 0 81 | 82 | numba_f1, numba_fd1 = rlplib.power_eps( sv1, 0.0, eps) 83 | python_f1, python_fd1 = rlplib.power_eps.py_func( sv1, 0.0, eps) 84 | 85 | assert np.allclose(numba_f1, python_f1) 86 | assert np.allclose(numba_fd1, python_fd1) 87 | 88 | # check that numba compiled version equals python version for N = 0.3 89 | 90 | numba_f2, numba_fd2 = rlplib.power_eps( sv1, 0.3, eps) 91 | python_f2, python_fd2 = rlplib.power_eps.py_func( sv1, 0.3, eps) 92 | 93 | assert np.allclose(numba_f2, python_f2) 94 | assert np.allclose(numba_fd2, python_fd2) 95 | 96 | # check that numba compiled version equals python version for N = 1.0 97 | 98 | numba_f3, numba_fd3 = rlplib.power_eps( sv1, 1.0, eps) 99 | python_f3, python_fd3 = rlplib.power_eps.py_func( sv1, 1.0, eps) 100 | 101 | assert np.allclose(numba_f3, python_f3) 102 | assert np.allclose(numba_fd3, python_fd3) 103 | 104 | # check that numba compiled version equals python version for N = 3.1 105 | 106 | numba_f4, numba_fd4 = rlplib.power_eps( sv1, 3.1, eps) 107 | python_f4, python_fd4 = rlplib.power_eps.py_func( sv1, 3.1, eps) 108 | 109 | assert np.allclose(numba_f4, python_f4) 110 | assert np.allclose(numba_fd4, python_fd4) 111 | 112 | def test_power_eps4(): 113 | 114 | eps = 0.0 115 | 116 | sv1 = np.logspace(-6, 0, 51) 117 | 118 | # check that for eps=0 power_eps equals standard np.power for N = 0.0 119 | 120 | numba_f1, numba_fd1 = rlplib.power_eps( sv1, 0.0, eps) 121 | numpy_f1, numpy_fd1 = np.power( sv1, 0.0), np.power(sv1, -1.0)*0.0 122 | 123 | assert np.allclose(numba_f1, numpy_f1) 124 | assert np.allclose(numba_fd1, numpy_fd1) 125 | 126 | # check that for eps=0 power_eps equals standard np.power for N = 0.3 127 | 128 | numba_f2, numba_fd2 = rlplib.power_eps( sv1, 0.3, eps) 129 | numpy_f2, numpy_fd2 = np.power( sv1, 0.3), np.power( sv1, 0.3-1.0 )*0.3 130 | 131 | assert np.allclose(numba_f2, numpy_f2) 132 | assert np.allclose(numba_fd2, numpy_fd2) 133 | 134 | # check that for eps=0 power_eps equals standard np.power for N = 1.0 135 | 136 | numba_f3, numba_fd3 = rlplib.power_eps( sv1, 1.0, eps) 137 | numpy_f3, numpy_fd3 = np.power( sv1, 1.0), np.power( sv1, 1.0-1.0 )*1.0 138 | 139 | assert np.allclose(numba_f3, numpy_f3) 140 | assert np.allclose(numba_fd3, numpy_fd3) 141 | 142 | # check that for eps=0 power_eps equals standard np.power for N = 3.1 143 | 144 | numba_f4, numba_fd4 = rlplib.power_eps( sv1, 3.1, eps) 145 | numpy_f4, numpy_fd4 = np.power( sv1, 3.1), np.power( sv1, 3.1-1.0 )*3.1 146 | 147 | assert np.allclose(numba_f4, numpy_f4) 148 | assert np.allclose(numba_fd4, numpy_fd4) 149 | 150 | def test_power_eps5(): 151 | 152 | eps = 1e-8 153 | 154 | 155 | sv1 = np.logspace(-6, 0, 51) 156 | 157 | # check that N is clipped to zero when N < 0 158 | 159 | numba_f1, numba_fd1 = rlplib.power_eps( sv1, -3.0, eps) 160 | ref_f1, ref_fd1 = rlplib.power_eps( sv1, 0.0, eps) 161 | 162 | assert np.allclose( numba_f1, ref_f1 ) 163 | assert np.allclose( numba_fd1, ref_fd1 ) 164 | 165 | # check that s is clipped to zero when s < 0, N = 3.0 166 | sv2 = -np.logspace(-6, 0, 51) 167 | 168 | numba_f2, numba_fd2 = rlplib.power_eps( sv2, 3.0, eps) 169 | ref_f2, ref_fd2 = rlplib.power_eps( 0.0, 3.0, eps) 170 | 171 | assert np.allclose( numba_f2, ref_f2 ) 172 | assert np.allclose( numba_fd2, ref_fd2 ) 173 | 174 | # check that s is clipped to zero when s < 0, N = 1.0 175 | sv3 = -np.logspace(-6, 0, 51) 176 | 177 | numba_f3, numba_fd3 = rlplib.power_eps( sv3, 1.0, eps) 178 | ref_f3, ref_fd3 = rlplib.power_eps( 0.0, 1.0, eps) 179 | 180 | assert np.allclose( numba_f3, ref_f3 ) 181 | assert np.allclose( numba_fd3, ref_fd3 ) 182 | 183 | # check that s is clipped to zero when s < 0, N = 0.5 184 | sv4 = -np.logspace(-6, 0, 51) 185 | 186 | numba_f4, numba_fd4 = rlplib.power_eps( sv4, 0.5, eps) 187 | ref_f4, ref_fd4 = rlplib.power_eps( 0.0, 0.5, eps) 188 | 189 | assert np.allclose( numba_f4, ref_f4 ) 190 | assert np.allclose( numba_fd4, ref_fd4 ) 191 | 192 | # check that s is clipped to zero when s < 0, N = 0.0 193 | sv5 = -np.logspace(-6, 0, 51) 194 | 195 | numba_f5, numba_fd5 = rlplib.power_eps( sv5, 0.0, eps) 196 | ref_f5, ref_fd5 = rlplib.power_eps( 0.0, 0.0, eps) 197 | 198 | assert np.allclose( numba_f5, ref_f5 ) 199 | assert np.allclose( numba_fd5, ref_fd5 ) 200 | 201 | # check that s is clipped to zero when s < 0, and N to zero when N = -1.0 202 | sv6 = -np.logspace(-6, 0, 51) 203 | 204 | numba_f6, numba_fd6 = rlplib.power_eps( sv6, -1.0, eps) 205 | ref_f6, ref_fd6 = rlplib.power_eps( 0.0, 0.0, eps) 206 | 207 | assert np.allclose( numba_f6, ref_f6 ) 208 | assert np.allclose( numba_fd6, ref_fd6 ) 209 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_relperm_Corey1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import pytest 7 | import numpy as np 8 | from scallib001 import relpermlib001 as rlplib 9 | 10 | 11 | def test_Corey_regular_derivatives1(): 12 | 13 | # When corey exponent < 1 infinite saturation derivatives occur at the end points, unless regulated using eps>0 14 | swi = 0.20 15 | sor = 0.15 16 | krw = 0.35 17 | kro = 0.80 18 | lw = 0.5 # lw < 1, would cause infinite derivative at endpoint 19 | lo = 0.5 # lo < 1, would cause infinite derivative at endpoint 20 | 21 | eps = 1e-4 22 | 23 | rlp_model = rlplib.Rlp2PCorey( 24 | swi, 25 | sor, 26 | lw, 27 | lo, 28 | krw, 29 | kro, 30 | eps=eps, 31 | ) 32 | 33 | # Choose to evaluate at the endpoints 34 | swv = np.array([swi, 1-sor]) 35 | 36 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 37 | 38 | assert np.isfinite(drlpo[0]) 39 | assert np.isfinite(drlpw[1]) 40 | 41 | def test_Corey_linear_model(): 42 | 43 | # Check derivatives for linear relperm 44 | swi = 0.00 45 | sor = 0.00 46 | krw = 1.00 47 | kro = 1.00 48 | lw = 1.0 49 | lo = 1.0 50 | 51 | eps = 1e-4 52 | 53 | rlp_model = rlplib.Rlp2PCorey( 54 | swi, 55 | sor, 56 | lw, 57 | lo, 58 | krw, 59 | kro, 60 | eps=eps, 61 | ) 62 | 63 | # Choose to evaluate including the endpoints 64 | swv = np.linspace(swi, 1-sor, 11) 65 | 66 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 67 | 68 | print('rlpw', rlpw) 69 | print('rlpo', rlpo) 70 | print('drlpw', drlpw) 71 | print('drlpo', drlpo) 72 | 73 | assert np.allclose( rlpw, swv ) 74 | assert np.allclose( rlpo, 1.0-swv ) 75 | assert np.allclose( drlpw, +1.0 ) 76 | assert np.allclose( drlpo, -1.0 ) 77 | 78 | def test_Corey_endpoints(): 79 | 80 | swi = 0.20 81 | sor = 0.15 82 | krw = 0.35 83 | kro = 0.80 84 | lw = 2.0 85 | lo = 3.0 86 | 87 | eps = 1e-4 88 | 89 | rlp_model = rlplib.Rlp2PCorey( 90 | swi, 91 | sor, 92 | lw, 93 | lo, 94 | krw, 95 | kro, 96 | eps=eps, 97 | ) 98 | 99 | # Choose to evaluate including the endpoints 100 | swv = np.array([0.0, swi, 1-sor, 1.0]) 101 | 102 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 103 | 104 | assert np.allclose( rlpw, [0.0, 0.0, krw, krw] ) 105 | assert np.allclose( rlpo, [kro, kro, 0.0, 0.0] ) 106 | assert np.allclose( [drlpw[0], drlpw[-1]], [0.0, 0.0] ) 107 | assert np.allclose( [drlpo[0], drlpo[-1]], [0.0, 0.0] ) 108 | 109 | def test_Corey_derivative1(): 110 | 111 | Sr1 = 0.20 112 | Sr2 = 0.15 113 | Ke1 = 0.35 114 | Ke2 = 0.80 115 | N1 = 2.4 116 | N2 = 3.0 117 | 118 | eps = 1e-4 119 | 120 | rlp_model = rlplib.Rlp2PCorey(Sr1=Sr1,Sr2=Sr2, 121 | N1=N1,N2=N2, 122 | Ke1=Ke1,Ke2=Ke2, 123 | eps=eps) 124 | 125 | deps = 0.01 126 | 127 | swv = np.linspace(Sr1+deps, 1-Sr2-deps, 1001) 128 | 129 | krw, kro, dkrw, dkro = rlp_model.calc(swv) 130 | 131 | dkrw_num = np.gradient(krw, swv, edge_order=2) 132 | dkro_num = np.gradient(kro, swv, edge_order=2) 133 | 134 | print('dkrw abs error', np.abs(dkrw - dkrw_num).max(), 'rel error', np.abs((dkrw - dkrw_num)/dkrw).max()) 135 | print('dkro abs error', np.abs(dkro - dkro_num).max(), 'rel error', np.abs((dkro - dkro_num)/dkro).max()) 136 | 137 | assert np.allclose(dkrw, dkrw_num, atol=1e-3, rtol=1e-2) 138 | assert np.allclose(dkro, dkro_num, atol=1e-3, rtol=1e-2) 139 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_relperm_LET1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import pytest 7 | import numpy as np 8 | from scallib001 import relpermlib001 as rlplib 9 | 10 | 11 | def test_LET_regular_derivatives1(): 12 | 13 | # When T<1 infinite saturation derivatives occur at the end points, unless regulated using eps>0 14 | swi = 0.20 15 | sor = 0.15 16 | krw = 0.35 17 | kro = 0.80 18 | lw = 2.0 19 | ew = 0.5 20 | tw = 0.85 # Note: tw < 1, would cause infinite derivative 21 | lo = 3.0 22 | eo = 0.60 23 | to = 0.95 # Note: to < 1, would cause infinite derivative 24 | 25 | eps = 1e-4 26 | 27 | rlp_model = rlplib.Rlp2PLET( 28 | swi, 29 | sor, 30 | lw, 31 | ew, 32 | tw, 33 | lo, 34 | eo, 35 | to, 36 | krw, 37 | kro, 38 | eps=eps, 39 | ) 40 | 41 | # Choose to evaluate at the endpoints 42 | swv = np.array([swi, 1-sor]) 43 | 44 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 45 | 46 | assert np.isfinite(drlpo[0]) 47 | assert np.isfinite(drlpw[1]) 48 | 49 | def test_LET_linear_model(): 50 | 51 | # Check derivatives when L=1 and E=0 (linear relperm) 52 | swi = 0.00 53 | sor = 0.00 54 | krw = 1.00 55 | kro = 1.00 56 | lw = 1.0 57 | ew = 0.0 58 | tw = 0.85 # note not relevant when ew = 0 59 | lo = 1.0 60 | eo = 0.00 61 | to = 0.95 # note not relevant when e0 = 0 62 | 63 | eps = 1e-4 # note not relevant for this case 64 | 65 | rlp_model = rlplib.Rlp2PLET( 66 | swi, 67 | sor, 68 | lw, 69 | ew, 70 | tw, 71 | lo, 72 | eo, 73 | to, 74 | krw, 75 | kro, 76 | eps=eps, 77 | ) 78 | 79 | # Choose to evaluate including the endpoints 80 | swv = np.linspace(swi, 1-sor, 11) 81 | 82 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 83 | 84 | print('rlpw', rlpw) 85 | print('rlpo', rlpo) 86 | print('drlpw', drlpw) 87 | print('drlpo', drlpo) 88 | 89 | assert np.allclose( rlpw, swv ) 90 | assert np.allclose( rlpo, 1.0-swv ) 91 | assert np.allclose( drlpw, +1.0 ) 92 | assert np.allclose( drlpo, -1.0 ) 93 | 94 | def test_LET_endpoints1(): 95 | 96 | # uses corey exponents < 1 97 | 98 | swi = 0.20 99 | sor = 0.15 100 | krw = 0.35 101 | kro = 0.80 102 | lw = 2.0 103 | ew = 0.5 104 | tw = 0.85 # Note: tw < 1 105 | lo = 3.0 106 | eo = 0.60 107 | to = 0.95 # Note: to < 1 108 | 109 | eps = 1e-4 110 | 111 | rlp_model = rlplib.Rlp2PLET( 112 | swi, 113 | sor, 114 | lw, 115 | ew, 116 | tw, 117 | lo, 118 | eo, 119 | to, 120 | krw, 121 | kro, 122 | eps=eps, 123 | ) 124 | 125 | # Choose to evaluate including the endpoints 126 | swv = np.array([0.0, swi, 1-sor, 1.0]) 127 | 128 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 129 | 130 | assert np.allclose( rlpw, [0.0, 0.0, krw, krw] ) 131 | assert np.allclose( rlpo, [kro, kro, 0.0, 0.0] ) 132 | assert np.allclose( [drlpw[0], drlpw[-1]], [0.0, 0.0] ) 133 | assert np.allclose( [drlpo[0], drlpo[-1]], [0.0, 0.0] ) 134 | 135 | def test_LET_endpoints2(): 136 | 137 | # uses corey exponents = 1 138 | 139 | swi = 0.20 140 | sor = 0.15 141 | krw = 0.35 142 | kro = 0.80 143 | lw = 2.0 144 | ew = 0.5 145 | tw = 1.00 146 | lo = 3.0 147 | eo = 0.60 148 | to = 1.00 149 | 150 | eps = 1e-4 151 | 152 | rlp_model = rlplib.Rlp2PLET( 153 | swi, 154 | sor, 155 | lw, 156 | ew, 157 | tw, 158 | lo, 159 | eo, 160 | to, 161 | krw, 162 | kro, 163 | eps=eps, 164 | ) 165 | 166 | # Choose to evaluate including the endpoints 167 | swv = np.array([0.0, swi, 1-sor, 1.0]) 168 | 169 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 170 | 171 | assert np.allclose( rlpw, [0.0, 0.0, krw, krw] ) 172 | assert np.allclose( rlpo, [kro, kro, 0.0, 0.0] ) 173 | assert np.allclose( [drlpw[0], drlpw[-1]], [0.0, 0.0] ) 174 | assert np.allclose( [drlpo[0], drlpo[-1]], [0.0, 0.0] ) 175 | 176 | def test_LET_endpoints3(): 177 | 178 | # uses corey exponents > 1 179 | 180 | swi = 0.20 181 | sor = 0.15 182 | krw = 0.35 183 | kro = 0.80 184 | lw = 2.0 185 | ew = 0.5 186 | tw = 2.00 187 | lo = 3.0 188 | eo = 0.60 189 | to = 2.00 190 | 191 | eps = 1e-4 192 | 193 | rlp_model = rlplib.Rlp2PLET( 194 | swi, 195 | sor, 196 | lw, 197 | ew, 198 | tw, 199 | lo, 200 | eo, 201 | to, 202 | krw, 203 | kro, 204 | eps=eps, 205 | ) 206 | 207 | # Choose to evaluate including the endpoints 208 | swv = np.array([0.0, swi, 1-sor, 1.0]) 209 | 210 | rlpw, rlpo, drlpw, drlpo = rlp_model.calc( swv ) 211 | 212 | assert np.allclose( rlpw, [0.0, 0.0, krw, krw] ) 213 | assert np.allclose( rlpo, [kro, kro, 0.0, 0.0] ) 214 | assert np.allclose( [drlpw[0], drlpw[-1]], [0.0, 0.0] ) 215 | assert np.allclose( [drlpo[0], drlpo[-1]], [0.0, 0.0] ) 216 | 217 | def test_LET_derivative1(): 218 | 219 | # uses corey exponents < 1 220 | 221 | Sr1 = 0.23 222 | Sr2 = 0.15 223 | Ke1 = 0.51 224 | Ke2 = 0.70 225 | L1 = 2.2 226 | E1 = 0.8 227 | T1 = 0.85 # note: T1 < 1 228 | L2 = 3.3 229 | E2 = 0.80 230 | T2 = 0.90 # note: T2 < 1 231 | 232 | eps = 1e-4 233 | 234 | rlp_model = rlplib.Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 235 | L1=L1,E1=E1,T1=T1, 236 | L2=L2,E2=E2,T2=T2, 237 | Ke1=Ke1,Ke2=Ke2, 238 | eps=eps) 239 | 240 | deps = 0.01 241 | 242 | swv = np.linspace(Sr1+deps, 1-Sr2-deps, 1001) 243 | 244 | krw, kro, dkrw, dkro = rlp_model.calc(swv) 245 | 246 | dkrw_num = np.gradient(krw, swv, edge_order=2) 247 | dkro_num = np.gradient(kro, swv, edge_order=2) 248 | 249 | print('dkrw abs error', np.abs(dkrw - dkrw_num).max(), 'rel error', np.abs((dkrw - dkrw_num)/dkrw).max()) 250 | print('dkro abs error', np.abs(dkro - dkro_num).max(), 'rel error', np.abs((dkro - dkro_num)/dkro).max()) 251 | 252 | assert np.allclose(dkrw, dkrw_num, atol=1e-3, rtol=1e-2) 253 | assert np.allclose(dkro, dkro_num, atol=1e-3, rtol=1e-2) 254 | 255 | def test_LET_derivative2(): 256 | 257 | # uses corey exponents = 1 258 | 259 | Sr1 = 0.23 260 | Sr2 = 0.15 261 | Ke1 = 0.51 262 | Ke2 = 0.70 263 | L1 = 2.2 264 | E1 = 0.8 265 | T1 = 1.00 266 | L2 = 3.3 267 | E2 = 0.80 268 | T2 = 1.00 269 | 270 | eps = 1e-4 271 | 272 | rlp_model = rlplib.Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 273 | L1=L1,E1=E1,T1=T1, 274 | L2=L2,E2=E2,T2=T2, 275 | Ke1=Ke1,Ke2=Ke2, 276 | eps=eps) 277 | 278 | deps = 0.01 279 | 280 | swv = np.linspace(Sr1+deps, 1-Sr2-deps, 1001) 281 | 282 | krw, kro, dkrw, dkro = rlp_model.calc(swv) 283 | 284 | dkrw_num = np.gradient(krw, swv, edge_order=2) 285 | dkro_num = np.gradient(kro, swv, edge_order=2) 286 | 287 | print('dkrw abs error', np.abs(dkrw - dkrw_num).max(), 'rel error', np.abs((dkrw - dkrw_num)/dkrw).max()) 288 | print('dkro abs error', np.abs(dkro - dkro_num).max(), 'rel error', np.abs((dkro - dkro_num)/dkro).max()) 289 | 290 | assert np.allclose(dkrw, dkrw_num, atol=1e-3, rtol=1e-2) 291 | assert np.allclose(dkro, dkro_num, atol=1e-3, rtol=1e-2) 292 | 293 | def test_LET_derivative3(): 294 | 295 | # uses corey exponents > 1 296 | 297 | Sr1 = 0.23 298 | Sr2 = 0.15 299 | Ke1 = 0.51 300 | Ke2 = 0.70 301 | L1 = 2.2 302 | E1 = 0.8 303 | T1 = 2.00 304 | L2 = 3.3 305 | E2 = 0.80 306 | T2 = 2.00 307 | 308 | eps = 1e-4 309 | 310 | rlp_model = rlplib.Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 311 | L1=L1,E1=E1,T1=T1, 312 | L2=L2,E2=E2,T2=T2, 313 | Ke1=Ke1,Ke2=Ke2, 314 | eps=eps) 315 | 316 | deps = 0.01 317 | 318 | swv = np.linspace(Sr1+deps, 1-Sr2-deps, 1001) 319 | 320 | krw, kro, dkrw, dkro = rlp_model.calc(swv) 321 | 322 | dkrw_num = np.gradient(krw, swv, edge_order=2) 323 | dkro_num = np.gradient(kro, swv, edge_order=2) 324 | 325 | print('dkrw abs error', np.abs(dkrw - dkrw_num).max(), 'rel error', np.abs((dkrw - dkrw_num)/dkrw).max()) 326 | print('dkro abs error', np.abs(dkro - dkro_num).max(), 'rel error', np.abs((dkro - dkro_num)/dkro).max()) 327 | 328 | assert np.allclose(dkrw, dkrw_num, atol=1e-3, rtol=1e-2) 329 | assert np.allclose(dkro, dkro_num, atol=1e-3, rtol=1e-2) 330 | 331 | def test_LET_vs_Corey1(): 332 | 333 | # LET model defaults to Corey when E = 0 334 | 335 | Sr1 = 0.20 336 | Sr2 = 0.15 337 | Ke1 = 0.35 338 | Ke2 = 0.80 339 | L1 = 2.0 340 | E1 = 0.0 341 | T1 = 0.85 # irrelevant since E1 = 0 342 | L2 = 3.0 343 | E2 = 0.00 344 | T2 = 0.95 # irrelevant since E2 = 0 345 | 346 | eps = 1e-4 347 | 348 | rlp_model1 = rlplib.Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 349 | L1=L1,E1=E1,T1=T1, 350 | L2=L2,E2=E2,T2=T2, 351 | Ke1=Ke1,Ke2=Ke2, 352 | eps=eps) 353 | 354 | rlp_model2 = rlplib.Rlp2PCorey(Sr1=Sr1,Sr2=Sr2, 355 | N1=L1,N2=L2, 356 | Ke1=Ke1,Ke2=Ke2, 357 | eps=eps) 358 | deps = 0.01 359 | 360 | swv = np.linspace(Sr1+deps, 1-Sr2-deps, 1001) 361 | 362 | krw1, kro1, dkrw1, dkro1 = rlp_model1.calc(swv) 363 | krw2, kro2, dkrw2, dkro2 = rlp_model2.calc(swv) 364 | 365 | assert np.allclose(krw1, krw2) 366 | assert np.allclose(kro1, kro2) 367 | assert np.allclose(dkrw1, dkrw2) 368 | assert np.allclose(dkro1, dkro2) 369 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_relperm_input.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import pytest 7 | from scallib001 import relpermlib001 as rlplib 8 | 9 | def test_Rlp2PLET_input(): 10 | 11 | Sr1 = 0.20 12 | Sr2 = 0.15 13 | Ke1 = 0.35 14 | Ke2 = 0.80 15 | L1 = 2.0 16 | E1 = 0.5 17 | T1 = 0.85 # note: T1 < 1 18 | L2 = 3.0 19 | E2 = 0.60 20 | T2 = 0.95 # note: T2 < 1 21 | 22 | eps = 1e-4 23 | 24 | 25 | def Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 26 | L1=L1,E1=E1,T1=T1, 27 | L2=L2,E2=E2,T2=T2, 28 | Ke1=Ke1,Ke2=Ke2, 29 | eps=eps): 30 | 31 | rlp_model = rlplib.Rlp2PLET(Sr1=Sr1,Sr2=Sr2, 32 | L1=L1,E1=E1,T1=T1, 33 | L2=L2,E2=E2,T2=T2, 34 | Ke1=Ke1,Ke2=Ke2, 35 | eps=eps) 36 | 37 | with pytest.raises(ValueError): 38 | Rlp2PLET(Sr1=-Sr1) 39 | 40 | with pytest.raises(ValueError): 41 | Rlp2PLET(Sr2=-Sr2) 42 | 43 | with pytest.raises(ValueError): 44 | Rlp2PLET(L1=-L1) 45 | 46 | with pytest.raises(ValueError): 47 | Rlp2PLET(E1=-E1) 48 | 49 | with pytest.raises(ValueError): 50 | Rlp2PLET(T1=-T1) 51 | 52 | with pytest.raises(ValueError): 53 | Rlp2PLET(L2=-L2) 54 | 55 | with pytest.raises(ValueError): 56 | Rlp2PLET(E2=-E2) 57 | 58 | with pytest.raises(ValueError): 59 | Rlp2PLET(T2=-T2) 60 | 61 | with pytest.raises(ValueError): 62 | Rlp2PLET(Ke1=-Ke1) 63 | 64 | with pytest.raises(ValueError): 65 | Rlp2PLET(Ke2=-Ke2) 66 | 67 | with pytest.raises(ValueError): 68 | Rlp2PLET(eps=-eps) 69 | 70 | with pytest.raises(ValueError): 71 | Rlp2PLET(Sr1=2.0) 72 | 73 | with pytest.raises(ValueError): 74 | Rlp2PLET(Sr2=2.0) 75 | 76 | with pytest.raises(ValueError): 77 | Rlp2PLET(Sr1=1.0) 78 | 79 | with pytest.raises(ValueError): 80 | Rlp2PLET(Sr2=1.0) 81 | 82 | with pytest.raises(ValueError): 83 | Rlp2PLET(Sr1=0.6, Sr2=0.6) 84 | 85 | with pytest.raises(ValueError): 86 | Rlp2PLET(E1=1.0, T1=0.5, E2=1.0, T2=2.0, eps=0) 87 | 88 | with pytest.raises(ValueError): 89 | Rlp2PLET(E2=1.0, T2=0.5, E1=1.0, T1=2.0, eps=0) 90 | 91 | with pytest.raises(ValueError): 92 | Rlp2PLET(L1=0.5, L2=2.0, T1=2.0, T2=2.0, eps=0) 93 | 94 | with pytest.raises(ValueError): 95 | Rlp2PLET(L2=0.5, L1=2.0, T1=2.0, T2=2.0, eps=0) 96 | 97 | 98 | def test_Rlp2PCorey_input(): 99 | 100 | Sr1 = 0.20 101 | Sr2 = 0.15 102 | Ke1 = 0.35 103 | Ke2 = 0.80 104 | N1 = 2.0 105 | N2 = 3.0 106 | 107 | eps = 1e-4 108 | 109 | 110 | def Rlp2PCorey(Sr1=Sr1,Sr2=Sr2, 111 | N1=N1, N2=N2, 112 | Ke1=Ke1,Ke2=Ke2, 113 | eps=eps): 114 | 115 | rlp_model = rlplib.Rlp2PCorey(Sr1=Sr1,Sr2=Sr2, 116 | N1=N1,N2=N2, 117 | Ke1=Ke1,Ke2=Ke2, 118 | eps=eps) 119 | 120 | with pytest.raises(ValueError): 121 | Rlp2PCorey(Sr1=-Sr1) 122 | 123 | with pytest.raises(ValueError): 124 | Rlp2PCorey(Sr2=-Sr2) 125 | 126 | with pytest.raises(ValueError): 127 | Rlp2PCorey(N1=-N1) 128 | 129 | with pytest.raises(ValueError): 130 | Rlp2PCorey(N2=-N2) 131 | 132 | with pytest.raises(ValueError): 133 | Rlp2PCorey(Ke1=-Ke1) 134 | 135 | with pytest.raises(ValueError): 136 | Rlp2PCorey(Ke2=-Ke2) 137 | 138 | with pytest.raises(ValueError): 139 | Rlp2PCorey(eps=-eps) 140 | 141 | with pytest.raises(ValueError): 142 | Rlp2PCorey(Sr1=2.0) 143 | 144 | with pytest.raises(ValueError): 145 | Rlp2PCorey(Sr2=2.0) 146 | 147 | with pytest.raises(ValueError): 148 | Rlp2PCorey(Sr1=1.0) 149 | 150 | with pytest.raises(ValueError): 151 | Rlp2PCorey(Sr2=1.0) 152 | 153 | with pytest.raises(ValueError): 154 | Rlp2PCorey(Sr1=0.6, Sr2=0.6) 155 | 156 | with pytest.raises(ValueError): 157 | Rlp2PCorey(N1=0.5, N2=2.0, eps=0) 158 | 159 | with pytest.raises(ValueError): 160 | Rlp2PCorey(N2=0.5, N1=2.0, eps=0) 161 | 162 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_relpermlib1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import numpy as np 7 | 8 | import scallib001.relpermlib001 as rlplib 9 | 10 | KRWE = 0.65 11 | KROE = 0.759 12 | SWC = 0.08 13 | SORW = 0.14 14 | NW = 2.5 15 | NOW = 3.0 16 | 17 | rlp_corey1 = rlplib.Rlp2PCorey(SWC, SORW, NW, NOW, KRWE, KROE) 18 | 19 | cpr_data_sat = np.array( 20 | [0.08, 0.1, 0.15, 0.3, 0.4, 0.5, 0.59, 0.68, 0.73, 0.78, 0.81, 0.82, 0.858, 0.86] 21 | ) 22 | cpr_data_cpr = np.array( 23 | [ 24 | -0.0, 25 | -0.001, 26 | -0.0054, 27 | -0.015, 28 | -0.0193, 29 | -0.0241, 30 | -0.0435, 31 | -0.0923, 32 | -0.1284, 33 | -0.18, 34 | -0.21, 35 | -0.24, 36 | -0.9, 37 | -1.2, 38 | ] 39 | ) 40 | 41 | cpr_cubic1_lex0_rex0 = rlplib.CubicInterpolator( 42 | cpr_data_sat, cpr_data_cpr, lex=0, rex=0 43 | ) 44 | cpr_cubic1_lex1_rex0 = rlplib.CubicInterpolator( 45 | cpr_data_sat, cpr_data_cpr, lex=1, rex=0 46 | ) 47 | cpr_cubic1_lex0_rex1 = rlplib.CubicInterpolator( 48 | cpr_data_sat, cpr_data_cpr, lex=0, rex=1 49 | ) 50 | cpr_cubic1_lex1_rex1 = rlplib.CubicInterpolator( 51 | cpr_data_sat, cpr_data_cpr, lex=1, rex=1 52 | ) 53 | 54 | sv = np.linspace(SWC, 1 - SORW, 51) 55 | 56 | kr1 = rlp_corey1.calc_kr1(sv) 57 | kr2 = rlp_corey1.calc_kr2(sv) 58 | 59 | 60 | rlp_cubic1_lex0_rex0 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=0, rex=0) 61 | rlp_cubic1_lex0_rex1 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=0, rex=1) 62 | rlp_cubic1_lex1_rex0 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=1, rex=0) 63 | rlp_cubic1_lex1_rex1 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=1, rex=1) 64 | 65 | Swc = 0.08 66 | Sorw = 0.14 67 | krwe = 0.65 68 | kroe = 0.759 69 | Lw = 1.50000000 70 | Ew = 9.64238306 71 | Tw = 1.27247992 72 | Lo = 2.05310839 73 | Eo = 3.34184238 74 | To = 1.00000000 75 | 76 | rlp_LET1 = rlplib.Rlp2PLET(Swc, Sorw, Lw, Ew, Tw, Lo, Eo, To, krwe, kroe) 77 | 78 | 79 | def test_cpr_cubic1(): 80 | 81 | assert np.allclose( 82 | cpr_cubic1_lex0_rex0.calc(np.array([0.000000]))[0], -0.00000000000000e00 83 | ) 84 | assert np.allclose( 85 | cpr_cubic1_lex0_rex0.calc(np.array([0.300000]))[0], -1.50000000000000e-02 86 | ) 87 | assert np.allclose( 88 | cpr_cubic1_lex0_rex0.calc(np.array([0.600000]))[0], -4.69908046260090e-02 89 | ) 90 | assert np.allclose( 91 | cpr_cubic1_lex0_rex0.calc(np.array([1.000000]))[0], -1.20000000000000e00 92 | ) 93 | 94 | assert np.allclose( 95 | cpr_cubic1_lex0_rex1.calc(np.array([0.000000]))[0], -0.00000000000000e00 96 | ) 97 | assert np.allclose( 98 | cpr_cubic1_lex0_rex1.calc(np.array([0.300000]))[0], -1.50000000000000e-02 99 | ) 100 | assert np.allclose( 101 | cpr_cubic1_lex0_rex1.calc(np.array([0.600000]))[0], -4.69908046260090e-02 102 | ) 103 | assert np.allclose( 104 | cpr_cubic1_lex0_rex1.calc(np.array([1.000000]))[0], -2.21999999999810e01 105 | ) 106 | 107 | assert np.allclose( 108 | cpr_cubic1_lex1_rex0.calc(np.array([0.000000]))[0], 4.00000000000000e-03 109 | ) 110 | assert np.allclose( 111 | cpr_cubic1_lex1_rex0.calc(np.array([0.300000]))[0], -1.50000000000000e-02 112 | ) 113 | assert np.allclose( 114 | cpr_cubic1_lex1_rex0.calc(np.array([0.600000]))[0], -4.69908046260090e-02 115 | ) 116 | assert np.allclose( 117 | cpr_cubic1_lex1_rex0.calc(np.array([1.000000]))[0], -1.20000000000000e00 118 | ) 119 | 120 | assert np.allclose( 121 | cpr_cubic1_lex1_rex1.calc(np.array([0.000000]))[0], 4.00000000000000e-03 122 | ) 123 | assert np.allclose( 124 | cpr_cubic1_lex1_rex1.calc(np.array([0.300000]))[0], -1.50000000000000e-02 125 | ) 126 | assert np.allclose( 127 | cpr_cubic1_lex1_rex1.calc(np.array([0.600000]))[0], -4.69908046260090e-02 128 | ) 129 | assert np.allclose( 130 | cpr_cubic1_lex1_rex1.calc(np.array([1.000000]))[0], -2.21999999999810e01 131 | ) 132 | 133 | 134 | def test_cpr_cubic_drv1(): 135 | 136 | assert np.allclose( 137 | cpr_cubic1_lex0_rex0.calc(np.array([0.000000]))[1], 0.00000000000000e00 138 | ) 139 | assert np.allclose( 140 | cpr_cubic1_lex0_rex0.calc(np.array([0.300000]))[1], -5.07749077490775e-02 141 | ) 142 | assert np.allclose( 143 | cpr_cubic1_lex0_rex0.calc(np.array([0.600000]))[1], -3.87853770059434e-01 144 | ) 145 | assert np.allclose( 146 | cpr_cubic1_lex0_rex0.calc(np.array([1.000000]))[1], 0.00000000000000e00 147 | ) 148 | 149 | assert np.allclose( 150 | cpr_cubic1_lex0_rex1.calc(np.array([0.000000]))[1], 0.00000000000000e00 151 | ) 152 | assert np.allclose( 153 | cpr_cubic1_lex0_rex1.calc(np.array([0.300000]))[1], -5.07749077490775e-02 154 | ) 155 | assert np.allclose( 156 | cpr_cubic1_lex0_rex1.calc(np.array([0.600000]))[1], -3.87853770059434e-01 157 | ) 158 | assert np.allclose( 159 | cpr_cubic1_lex0_rex1.calc(np.array([1.000000]))[1], -1.49999999999590e02 160 | ) 161 | 162 | assert np.allclose( 163 | cpr_cubic1_lex1_rex0.calc(np.array([0.000000]))[1], -5.00000000000000e-02 164 | ) 165 | assert np.allclose( 166 | cpr_cubic1_lex1_rex0.calc(np.array([0.300000]))[1], -5.07749077490775e-02 167 | ) 168 | assert np.allclose( 169 | cpr_cubic1_lex1_rex0.calc(np.array([0.600000]))[1], -3.87853770059434e-01 170 | ) 171 | assert np.allclose( 172 | cpr_cubic1_lex1_rex0.calc(np.array([1.000000]))[1], 0.00000000000000e00 173 | ) 174 | 175 | assert np.allclose( 176 | cpr_cubic1_lex1_rex1.calc(np.array([0.000000]))[1], -5.00000000000000e-02 177 | ) 178 | assert np.allclose( 179 | cpr_cubic1_lex1_rex1.calc(np.array([0.300000]))[1], -5.07749077490775e-02 180 | ) 181 | assert np.allclose( 182 | cpr_cubic1_lex1_rex1.calc(np.array([0.600000]))[1], -3.87853770059434e-01 183 | ) 184 | assert np.allclose( 185 | cpr_cubic1_lex1_rex1.calc(np.array([1.000000]))[1], -1.49999999999590e02 186 | ) 187 | 188 | 189 | def test_corey1(): 190 | 191 | assert np.allclose(rlp_corey1.calc_kr1(np.array([0.000000])), 0.00000000000000e00) 192 | assert np.allclose(rlp_corey1.calc_kr1(np.array([0.300000])), 2.74620878417945e-02) 193 | assert np.allclose(rlp_corey1.calc_kr1(np.array([0.600000])), 2.35876790045787e-01) 194 | assert np.allclose(rlp_corey1.calc_kr1(np.array([1.000000])), 6.50000000000000e-01) 195 | assert np.allclose(rlp_corey1.calc_kr2(np.array([0.000000])), 7.59000000000000e-01) 196 | assert np.allclose(rlp_corey1.calc_kr2(np.array([0.300000])), 2.80880797046478e-01) 197 | assert np.allclose(rlp_corey1.calc_kr2(np.array([0.600000])), 2.81111111111111e-02) 198 | assert np.allclose(rlp_corey1.calc_kr2(np.array([1.000000])), 0.00000000000000e00) 199 | assert np.allclose( 200 | rlp_corey1.calc_kr1_der(np.array([0.000000])), 0.00000000000000e00 201 | ) 202 | assert np.allclose( 203 | rlp_corey1.calc_kr1_der(np.array([0.300000])), 3.12069180020392e-01 204 | ) 205 | assert np.allclose( 206 | rlp_corey1.calc_kr1_der(np.array([0.600000])), 1.13402302906629e00 207 | ) 208 | assert np.allclose( 209 | rlp_corey1.calc_kr1_der(np.array([1.000000])), 0.00000000000000e00 210 | ) 211 | assert np.allclose( 212 | rlp_corey1.calc_kr2_der(np.array([0.000000])), 0.00000000000000e00 213 | ) 214 | assert np.allclose( 215 | rlp_corey1.calc_kr2_der(np.array([0.300000])), -1.50471855560613e00 216 | ) 217 | assert np.allclose( 218 | rlp_corey1.calc_kr2_der(np.array([0.600000])), -3.24358974358974e-01 219 | ) 220 | assert np.allclose( 221 | rlp_corey1.calc_kr2_der(np.array([1.000000])), 0.00000000000000e00 222 | ) 223 | 224 | 225 | def test_LET1(): 226 | 227 | assert np.allclose(rlp_LET1.calc_kr1(np.array([0.000000])), 0.00000000000000e00) 228 | assert np.allclose(rlp_LET1.calc_kr1(np.array([0.300000])), 1.50374459293526e-02) 229 | assert np.allclose(rlp_LET1.calc_kr1(np.array([0.600000])), 1.20881285388824e-01) 230 | assert np.allclose(rlp_LET1.calc_kr1(np.array([1.000000])), 6.50000000000000e-01) 231 | assert np.allclose(rlp_LET1.calc_kr2(np.array([0.000000])), 7.59000000000000e-01) 232 | assert np.allclose(rlp_LET1.calc_kr2(np.array([0.300000])), 2.65282532284267e-01) 233 | assert np.allclose(rlp_LET1.calc_kr2(np.array([0.600000])), 3.41035522091661e-02) 234 | assert np.allclose(rlp_LET1.calc_kr2(np.array([1.000000])), 0.00000000000000e00) 235 | assert np.allclose(rlp_LET1.calc_kr1_der(np.array([0.000000])), 0.00000000000000e00) 236 | assert np.allclose( 237 | rlp_LET1.calc_kr1_der(np.array([0.300000])), 1.33534981167988e-01 238 | ) 239 | assert np.allclose( 240 | rlp_LET1.calc_kr1_der(np.array([0.600000])), 7.65437448200503e-01 241 | ) 242 | assert np.allclose(rlp_LET1.calc_kr1_der(np.array([1.000000])), 0.00000000000000e00) 243 | assert np.allclose(rlp_LET1.calc_kr2_der(np.array([0.000000])), 0.00000000000000e00) 244 | assert np.allclose( 245 | rlp_LET1.calc_kr2_der(np.array([0.300000])), -1.41703141664956e00 246 | ) 247 | assert np.allclose( 248 | rlp_LET1.calc_kr2_der(np.array([0.600000])), -3.19837747167613e-01 249 | ) 250 | assert np.allclose(rlp_LET1.calc_kr2_der(np.array([1.000000])), 0.00000000000000e00) 251 | 252 | 253 | def test_cubic1(): 254 | 255 | assert np.allclose( 256 | rlp_cubic1_lex0_rex0.calc_kr1(np.array([0.000000])), 0.00000000000000e00 257 | ) 258 | assert np.allclose( 259 | rlp_cubic1_lex0_rex0.calc_kr1(np.array([0.300000])), 2.74612991537836e-02 260 | ) 261 | assert np.allclose( 262 | rlp_cubic1_lex0_rex0.calc_kr1(np.array([0.600000])), 2.35876264264027e-01 263 | ) 264 | assert np.allclose( 265 | rlp_cubic1_lex0_rex0.calc_kr1(np.array([1.000000])), 6.50000000000000e-01 266 | ) 267 | assert np.allclose( 268 | rlp_cubic1_lex0_rex0.calc_kr2(np.array([0.000000])), 7.59000000000000e-01 269 | ) 270 | assert np.allclose( 271 | rlp_cubic1_lex0_rex0.calc_kr2(np.array([0.300000])), 2.80881685206823e-01 272 | ) 273 | assert np.allclose( 274 | rlp_cubic1_lex0_rex0.calc_kr2(np.array([0.600000])), 2.81120093122634e-02 275 | ) 276 | assert np.allclose( 277 | rlp_cubic1_lex0_rex0.calc_kr2(np.array([1.000000])), 0.00000000000000e00 278 | ) 279 | assert np.allclose( 280 | rlp_cubic1_lex0_rex0.calc_kr1_der(np.array([0.000000])), 0.00000000000000e00 281 | ) 282 | assert np.allclose( 283 | rlp_cubic1_lex0_rex0.calc_kr1_der(np.array([0.300000])), 3.11757260370099e-01 284 | ) 285 | assert np.allclose( 286 | rlp_cubic1_lex0_rex0.calc_kr1_der(np.array([0.600000])), 1.13417044470738e00 287 | ) 288 | assert np.allclose( 289 | rlp_cubic1_lex0_rex0.calc_kr1_der(np.array([1.000000])), 0.00000000000000e00 290 | ) 291 | assert np.allclose( 292 | rlp_cubic1_lex0_rex0.calc_kr2_der(np.array([0.000000])), 0.00000000000000e00 293 | ) 294 | assert np.allclose( 295 | rlp_cubic1_lex0_rex0.calc_kr2_der(np.array([0.300000])), -1.50437014506902e00 296 | ) 297 | assert np.allclose( 298 | rlp_cubic1_lex0_rex0.calc_kr2_der(np.array([0.600000])), -3.24617955386615e-01 299 | ) 300 | assert np.allclose( 301 | rlp_cubic1_lex0_rex0.calc_kr2_der(np.array([1.000000])), 0.00000000000000e00 302 | ) 303 | 304 | assert np.allclose( 305 | rlp_cubic1_lex0_rex1.calc_kr1(np.array([0.000000])), 0.00000000000000e00 306 | ) 307 | assert np.allclose( 308 | rlp_cubic1_lex0_rex1.calc_kr1(np.array([0.300000])), 2.74612991537836e-02 309 | ) 310 | assert np.allclose( 311 | rlp_cubic1_lex0_rex1.calc_kr1(np.array([0.600000])), 2.35876264264027e-01 312 | ) 313 | assert np.allclose( 314 | rlp_cubic1_lex0_rex1.calc_kr1(np.array([1.000000])), 9.37306286678924e-01 315 | ) 316 | assert np.allclose( 317 | rlp_cubic1_lex0_rex1.calc_kr2(np.array([0.000000])), 7.59000000000000e-01 318 | ) 319 | assert np.allclose( 320 | rlp_cubic1_lex0_rex1.calc_kr2(np.array([0.300000])), 2.80881685206823e-01 321 | ) 322 | assert np.allclose( 323 | rlp_cubic1_lex0_rex1.calc_kr2(np.array([0.600000])), 2.81120093122634e-02 324 | ) 325 | assert np.allclose( 326 | rlp_cubic1_lex0_rex1.calc_kr2(np.array([1.000000])), -5.44923076923068e-05 327 | ) 328 | assert np.allclose( 329 | rlp_cubic1_lex0_rex1.calc_kr1_der(np.array([0.000000])), 0.00000000000000e00 330 | ) 331 | assert np.allclose( 332 | rlp_cubic1_lex0_rex1.calc_kr1_der(np.array([0.300000])), 3.11757260370099e-01 333 | ) 334 | assert np.allclose( 335 | rlp_cubic1_lex0_rex1.calc_kr1_der(np.array([0.600000])), 1.13417044470738e00 336 | ) 337 | assert np.allclose( 338 | rlp_cubic1_lex0_rex1.calc_kr1_der(np.array([1.000000])), 2.05218776199232e00 339 | ) 340 | assert np.allclose( 341 | rlp_cubic1_lex0_rex1.calc_kr2_der(np.array([0.000000])), 0.00000000000000e00 342 | ) 343 | assert np.allclose( 344 | rlp_cubic1_lex0_rex1.calc_kr2_der(np.array([0.300000])), -1.50437014506902e00 345 | ) 346 | assert np.allclose( 347 | rlp_cubic1_lex0_rex1.calc_kr2_der(np.array([0.600000])), -3.24617955386615e-01 348 | ) 349 | assert np.allclose( 350 | rlp_cubic1_lex0_rex1.calc_kr2_der(np.array([1.000000])), -3.89230769230756e-04 351 | ) 352 | 353 | assert np.allclose( 354 | rlp_cubic1_lex1_rex0.calc_kr1(np.array([0.000000])), -1.88561808316413e-04 355 | ) 356 | assert np.allclose( 357 | rlp_cubic1_lex1_rex0.calc_kr1(np.array([0.300000])), 2.74612991537836e-02 358 | ) 359 | assert np.allclose( 360 | rlp_cubic1_lex1_rex0.calc_kr1(np.array([0.600000])), 2.35876264264027e-01 361 | ) 362 | assert np.allclose( 363 | rlp_cubic1_lex1_rex0.calc_kr1(np.array([1.000000])), 6.50000000000000e-01 364 | ) 365 | assert np.allclose( 366 | rlp_cubic1_lex1_rex0.calc_kr2(np.array([0.000000])), 9.87898830769230e-01 367 | ) 368 | assert np.allclose( 369 | rlp_cubic1_lex1_rex0.calc_kr2(np.array([0.300000])), 2.80881685206823e-01 370 | ) 371 | assert np.allclose( 372 | rlp_cubic1_lex1_rex0.calc_kr2(np.array([0.600000])), 2.81120093122634e-02 373 | ) 374 | assert np.allclose( 375 | rlp_cubic1_lex1_rex0.calc_kr2(np.array([1.000000])), 0.00000000000000e00 376 | ) 377 | assert np.allclose( 378 | rlp_cubic1_lex1_rex0.calc_kr1_der(np.array([0.000000])), 2.35702260395516e-03 379 | ) 380 | assert np.allclose( 381 | rlp_cubic1_lex1_rex0.calc_kr1_der(np.array([0.300000])), 3.11757260370099e-01 382 | ) 383 | assert np.allclose( 384 | rlp_cubic1_lex1_rex0.calc_kr1_der(np.array([0.600000])), 1.13417044470738e00 385 | ) 386 | assert np.allclose( 387 | rlp_cubic1_lex1_rex0.calc_kr1_der(np.array([1.000000])), 0.00000000000000e00 388 | ) 389 | assert np.allclose( 390 | rlp_cubic1_lex1_rex0.calc_kr2_der(np.array([0.000000])), -2.86123538461536e00 391 | ) 392 | assert np.allclose( 393 | rlp_cubic1_lex1_rex0.calc_kr2_der(np.array([0.300000])), -1.50437014506902e00 394 | ) 395 | assert np.allclose( 396 | rlp_cubic1_lex1_rex0.calc_kr2_der(np.array([0.600000])), -3.24617955386615e-01 397 | ) 398 | assert np.allclose( 399 | rlp_cubic1_lex1_rex0.calc_kr2_der(np.array([1.000000])), 0.00000000000000e00 400 | ) 401 | 402 | assert np.allclose( 403 | rlp_cubic1_lex1_rex1.calc_kr1(np.array([0.000000])), -1.88561808316413e-04 404 | ) 405 | assert np.allclose( 406 | rlp_cubic1_lex1_rex1.calc_kr1(np.array([0.300000])), 2.74612991537836e-02 407 | ) 408 | assert np.allclose( 409 | rlp_cubic1_lex1_rex1.calc_kr1(np.array([0.600000])), 2.35876264264027e-01 410 | ) 411 | assert np.allclose( 412 | rlp_cubic1_lex1_rex1.calc_kr1(np.array([1.000000])), 9.37306286678924e-01 413 | ) 414 | assert np.allclose( 415 | rlp_cubic1_lex1_rex1.calc_kr2(np.array([0.000000])), 9.87898830769230e-01 416 | ) 417 | assert np.allclose( 418 | rlp_cubic1_lex1_rex1.calc_kr2(np.array([0.300000])), 2.80881685206823e-01 419 | ) 420 | assert np.allclose( 421 | rlp_cubic1_lex1_rex1.calc_kr2(np.array([0.600000])), 2.81120093122634e-02 422 | ) 423 | assert np.allclose( 424 | rlp_cubic1_lex1_rex1.calc_kr2(np.array([1.000000])), -5.44923076923068e-05 425 | ) 426 | assert np.allclose( 427 | rlp_cubic1_lex1_rex1.calc_kr1_der(np.array([0.000000])), 2.35702260395516e-03 428 | ) 429 | assert np.allclose( 430 | rlp_cubic1_lex1_rex1.calc_kr1_der(np.array([0.300000])), 3.11757260370099e-01 431 | ) 432 | assert np.allclose( 433 | rlp_cubic1_lex1_rex1.calc_kr1_der(np.array([0.600000])), 1.13417044470738e00 434 | ) 435 | assert np.allclose( 436 | rlp_cubic1_lex1_rex1.calc_kr1_der(np.array([1.000000])), 2.05218776199232e00 437 | ) 438 | assert np.allclose( 439 | rlp_cubic1_lex1_rex1.calc_kr2_der(np.array([0.000000])), -2.86123538461536e00 440 | ) 441 | assert np.allclose( 442 | rlp_cubic1_lex1_rex1.calc_kr2_der(np.array([0.300000])), -1.50437014506902e00 443 | ) 444 | assert np.allclose( 445 | rlp_cubic1_lex1_rex1.calc_kr2_der(np.array([0.600000])), -3.24617955386615e-01 446 | ) 447 | assert np.allclose( 448 | rlp_cubic1_lex1_rex1.calc_kr2_der(np.array([1.000000])), -3.89230769230756e-04 449 | ) 450 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_relpermlib_cpubench1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import numpy as np 7 | 8 | import scallib001.relpermlib001 as rlplib 9 | 10 | 11 | def test_setup_rlp_models( benchmark ): 12 | 13 | if benchmark is None: 14 | 15 | return 16 | 17 | else: 18 | 19 | KRWE = 0.65 20 | KROE = 0.759 21 | SWC = 0.08 22 | SORW = 0.14 23 | NW = 2.5 24 | NOW = 3.0 25 | 26 | rlp_corey1 = rlplib.Rlp2PCorey(SWC, SORW, NW, NOW, KRWE, KROE) 27 | 28 | cpr_data_sat = np.array( 29 | [0.08, 0.1, 0.15, 0.3, 0.4, 0.5, 0.59, 0.68, 0.73, 0.78, 0.81, 0.82, 0.858, 0.86] 30 | ) 31 | cpr_data_cpr = np.array( 32 | [ 33 | -0.0, 34 | -0.001, 35 | -0.0054, 36 | -0.015, 37 | -0.0193, 38 | -0.0241, 39 | -0.0435, 40 | -0.0923, 41 | -0.1284, 42 | -0.18, 43 | -0.21, 44 | -0.24, 45 | -0.9, 46 | -1.2, 47 | ] 48 | ) 49 | 50 | cpr_cubic1_lex0_rex0 = rlplib.CubicInterpolator( 51 | cpr_data_sat, cpr_data_cpr, lex=0, rex=0 52 | ) 53 | cpr_cubic1_lex1_rex0 = rlplib.CubicInterpolator( 54 | cpr_data_sat, cpr_data_cpr, lex=1, rex=0 55 | ) 56 | cpr_cubic1_lex0_rex1 = rlplib.CubicInterpolator( 57 | cpr_data_sat, cpr_data_cpr, lex=0, rex=1 58 | ) 59 | cpr_cubic1_lex1_rex1 = rlplib.CubicInterpolator( 60 | cpr_data_sat, cpr_data_cpr, lex=1, rex=1 61 | ) 62 | 63 | sv = np.linspace(SWC, 1 - SORW, 51) 64 | 65 | kr1 = rlp_corey1.calc_kr1(sv) 66 | kr2 = rlp_corey1.calc_kr2(sv) 67 | 68 | 69 | rlp_cubic1_lex0_rex0 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=0, rex=0) 70 | rlp_cubic1_lex0_rex1 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=0, rex=1) 71 | rlp_cubic1_lex1_rex0 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=1, rex=0) 72 | rlp_cubic1_lex1_rex1 = rlplib.Rlp2PCubic(sv, kr1, kr2, lex=1, rex=1) 73 | 74 | Swc = 0.08 75 | Sorw = 0.14 76 | krwe = 0.65 77 | kroe = 0.759 78 | Lw = 1.50000000 79 | Ew = 9.64238306 80 | Tw = 1.27247992 81 | Lo = 2.05310839 82 | Eo = 3.34184238 83 | To = 1.00000000 84 | 85 | rlp_LET1 = rlplib.Rlp2PLET(Swc, Sorw, Lw, Ew, Tw, Lo, Eo, To, krwe, kroe) 86 | 87 | 88 | sv101 = np.linspace(0, 1, 101) 89 | 90 | # Force jit compilation before benchmarking 91 | rlp_corey1.calc(sv101) 92 | rlp_cubic1_lex0_rex1.calc(sv101) 93 | rlp_LET1.calc(sv101) 94 | cpr_cubic1_lex0_rex1.calc(sv101) 95 | 96 | globals()['sv101'] = sv101 97 | globals()['sv1001'] = sv1001 98 | globals()['rlp_corey1'] = rlp_corey1 99 | globals()['rlp_cubic1_lex0_rex1'] = rlp_cubic1_lex0_rex1 100 | globals()['rlp_LET1'] = rlp_LET1 101 | globals()['cpr_cubic1_lex0_rex1'] = cpr_cubic1_lex0_rex1 102 | 103 | 104 | 105 | def test_rlp_corey1_sv101(benchmark): 106 | 107 | if benchmark is None: 108 | return 109 | 110 | def t(): 111 | rlp_corey1.calc(sv101) 112 | 113 | benchmark(t) 114 | 115 | 116 | def test_rlp_cubic1_sv101(benchmark): 117 | 118 | if benchmark is None: 119 | return 120 | 121 | def t(): 122 | rlp_cubic1_lex0_rex1.calc(sv101) 123 | 124 | benchmark(t) 125 | 126 | 127 | def test_rlp_LET1_sv101(benchmark): 128 | 129 | if benchmark is None: 130 | return 131 | 132 | def t(): 133 | rlp_LET1.calc(sv101) 134 | 135 | benchmark(t) 136 | 137 | 138 | def test_cpr_cubic1_sv101(benchmark): 139 | 140 | if benchmark is None: 141 | return 142 | 143 | def t(): 144 | cpr_cubic1_lex0_rex1.calc(sv101) 145 | 146 | benchmark(t) 147 | 148 | 149 | sv1001 = np.linspace(0, 1, 1001) 150 | 151 | 152 | def test_corey1_sv1001(benchmark): 153 | 154 | if benchmark is None: 155 | return 156 | 157 | def t(): 158 | rlp_corey1.calc(sv1001) 159 | 160 | benchmark(t) 161 | 162 | 163 | def test_cubic1_sv1001(benchmark): 164 | 165 | if benchmark is None: 166 | return 167 | 168 | def t(): 169 | rlp_cubic1_lex0_rex1.calc(sv1001) 170 | 171 | benchmark(t) 172 | 173 | 174 | def test_LET1_sv1001(benchmark): 175 | 176 | if benchmark is None: 177 | return 178 | 179 | def t(): 180 | rlp_LET1.calc(sv1001) 181 | 182 | benchmark(t) 183 | 184 | 185 | def test_cpr_cubic1_sv1001(benchmark): 186 | 187 | if benchmark is None: 188 | return 189 | 190 | def t(): 191 | cpr_cubic1_lex0_rex1.calc(sv1001) 192 | 193 | benchmark(t) 194 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_simulation1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | # Setup 7 | 8 | import numpy as np 9 | import pandas as pd 10 | 11 | from scallib001.displacementmodel1D2P001 import DisplacementModel1D2P 12 | import scallib001.relpermlib001 as rlplib 13 | 14 | 15 | def setup_and_solve(): 16 | 17 | KRWE = 0.65 18 | KROE = 0.759 19 | SWC = 0.08 20 | SORW = 0.14 21 | NW = 2.5 22 | NOW = 3.0 23 | 24 | rlp_model1 = rlplib.Rlp2PCorey(SWC, SORW, NW, NOW, KRWE, KROE) 25 | 26 | cpr_model1 = rlplib.CubicInterpolator( 27 | np.array( 28 | [ 29 | 0.08, 30 | 0.1, 31 | 0.15, 32 | 0.3, 33 | 0.4, 34 | 0.5, 35 | 0.59, 36 | 0.68, 37 | 0.73, 38 | 0.78, 39 | 0.81, 40 | 0.82, 41 | 0.858, 42 | 0.86, 43 | ] 44 | ), 45 | np.array( 46 | [ 47 | -0.0, 48 | -0.001, 49 | -0.0054, 50 | -0.015, 51 | -0.0193, 52 | -0.0241, 53 | -0.0435, 54 | -0.0923, 55 | -0.1284, 56 | -0.18, 57 | -0.21, 58 | -0.24, 59 | -0.9, 60 | -1.2, 61 | ] 62 | ), 63 | ) 64 | 65 | T_END = 200.0 66 | 67 | Movie_times = np.linspace(0, T_END, 200) 68 | 69 | Schedule = pd.DataFrame( 70 | [ 71 | [0.0, 1.0, 0.0], 72 | [5.2, 1.0, 0.01], 73 | [21.5, 1.0, 0.05], 74 | [37.6, 1.0, 0.1], 75 | [46.4, 1.0, 0.25], 76 | [62.5, 1.0, 0.5], 77 | [78.5, 1.0, 0.75], 78 | [94.6, 1.0, 0.9], 79 | [110.6, 1.0, 0.95], 80 | [128.3, 1.0, 0.99], 81 | [144.3, 1.0, 1.0], 82 | [166.4, 2.0, 1.0], 83 | [178.9, 5.0, 1.0], 84 | ], 85 | columns=["StartTime", "InjRate", "FracFlow"], 86 | ) 87 | 88 | # Schedule 89 | 90 | model1 = DisplacementModel1D2P( 91 | NX=50, 92 | core_length=3.9, # cm 93 | core_area=12.0, # cm2 94 | permeability=250.0, # mDarcy 95 | porosity=0.18, # v/v 96 | sw_initial=SWC, # v/v 97 | viscosity_w=1.0, # cP 98 | viscosity_n=2.0, # cP 99 | density_w=1000.0, # kg/m3 100 | density_n=800.0, # kg/m3 101 | rlp_model=rlp_model1, 102 | cpr_model=cpr_model1, 103 | time_end=T_END, # hour 104 | rate_schedule=Schedule, 105 | movie_schedule=Movie_times, 106 | ) 107 | 108 | results = model1.solve().results 109 | 110 | return results 111 | 112 | 113 | 114 | # Testing below 115 | 116 | 117 | def test_tss_table(RTOL=0.001): 118 | 119 | results = setup_and_solve() 120 | 121 | # check that simulation results are identical to reference simulation 122 | 123 | # data created at 2024-02-01 22:52:46.104500 124 | 125 | tss_table = results.tss_table 126 | 127 | assert np.allclose( tss_table.TIME .values[-1], 2.00000000E+02, rtol=RTOL) 128 | assert np.allclose( tss_table.DTIME .values[-1], 1.00502513E+00, rtol=RTOL) 129 | assert np.allclose( tss_table.PVinj .values[-1], 2.11467236E+03, rtol=RTOL) 130 | assert np.allclose( tss_table.tD .values[-1], 2.11467236E+03, rtol=RTOL) 131 | assert np.allclose( tss_table.dtD .values[-1], 3.57914931E+01, rtol=RTOL) 132 | assert np.allclose( tss_table.InjRate .values[-1], 5.00000000E+00, rtol=RTOL) 133 | assert np.allclose( tss_table.PVInjRate .values[-1], 5.93542260E-01, rtol=RTOL) 134 | assert np.allclose( tss_table.FracFlowInj .values[-1], 1.00000000E+00, rtol=RTOL) 135 | assert np.allclose( tss_table.FracFlowPrd .values[-1], 1.00000000E+00, rtol=RTOL) 136 | assert np.allclose( tss_table.WATERInj .values[-1], 5.00000000E+00, rtol=RTOL) 137 | assert np.allclose( tss_table.OILInj .values[-1], 0.00000000E+00, rtol=RTOL) 138 | assert np.allclose( tss_table.WATERProd .values[-1], 5.00000000E+00, rtol=RTOL) 139 | assert np.allclose( tss_table.CumWATERInj .values[-1], 1.35361800E+04, rtol=RTOL) 140 | assert np.allclose( tss_table.CumOILInj .values[-1], 4.27782000E+03, rtol=RTOL) 141 | assert np.allclose( tss_table.CumWATER .values[-1], 1.35305281E+04, rtol=RTOL) 142 | assert np.allclose( tss_table.CumOIL .values[-1], 4.28347193E+03, rtol=RTOL) 143 | assert np.allclose( tss_table.P_inj .values[-1], 1.27795742E+00, rtol=RTOL) 144 | assert np.allclose( tss_table.P_inj_wat .values[-1], 1.27783120E+00, rtol=RTOL) 145 | assert np.allclose( tss_table.P_inj_oil .values[-1], 1.00305627E+00, rtol=RTOL) 146 | assert np.allclose( tss_table.P_prod .values[-1], 1.00000000E+00, rtol=RTOL) 147 | assert np.allclose( tss_table.P_prod_wat .values[-1], 1.01114291E+00, rtol=RTOL) 148 | assert np.allclose( tss_table.P_prod_oil .values[-1], 1.00000340E+00, rtol=RTOL) 149 | assert np.allclose( tss_table.delta_P .values[-1], 2.77957419E-01, rtol=RTOL) 150 | assert np.allclose( tss_table.delta_P_w .values[-1], 2.66688294E-01, rtol=RTOL) 151 | assert np.allclose( tss_table.delta_P_o .values[-1], 3.05286721E-03, rtol=RTOL) 152 | 153 | assert results.movie_nr.sum() == 424 154 | -------------------------------------------------------------------------------- /python/scallib001/tests/test_solver_cpubench1.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | from scallib001.displacementmodel1D2P001 import DisplacementModel1D2P 10 | import scallib001.relpermlib001 as rlplib 11 | 12 | def test_solver1(benchmark): 13 | 14 | if benchmark is None: 15 | return 16 | 17 | KRWE = 0.65 18 | KROE = 0.759 19 | SWC = 0.08 20 | SORW = 0.14 21 | NW = 2.5 22 | NOW = 3.0 23 | 24 | rlp_model1 = rlplib.Rlp2PCorey(SWC, SORW, NW, NOW, KRWE, KROE) 25 | 26 | cpr_model1 = rlplib.CubicInterpolator( 27 | np.array( 28 | [ 29 | 0.08, 30 | 0.1, 31 | 0.15, 32 | 0.3, 33 | 0.4, 34 | 0.5, 35 | 0.59, 36 | 0.68, 37 | 0.73, 38 | 0.78, 39 | 0.81, 40 | 0.82, 41 | 0.858, 42 | 0.86, 43 | ] 44 | ), 45 | np.array( 46 | [ 47 | -0.0, 48 | -0.001, 49 | -0.0054, 50 | -0.015, 51 | -0.0193, 52 | -0.0241, 53 | -0.0435, 54 | -0.0923, 55 | -0.1284, 56 | -0.18, 57 | -0.21, 58 | -0.24, 59 | -0.9, 60 | -1.2, 61 | ] 62 | ), 63 | ) 64 | 65 | T_END = 200.0 66 | 67 | Movie_times = np.linspace(0, T_END, 200) 68 | 69 | Schedule = pd.DataFrame( 70 | [ 71 | [0.0, 1.0, 0.0], 72 | [5.2, 1.0, 0.01], 73 | [21.5, 1.0, 0.05], 74 | [37.6, 1.0, 0.1], 75 | [46.4, 1.0, 0.25], 76 | [62.5, 1.0, 0.5], 77 | [78.5, 1.0, 0.75], 78 | [94.6, 1.0, 0.9], 79 | [110.6, 1.0, 0.95], 80 | [128.3, 1.0, 0.99], 81 | [144.3, 1.0, 1.0], 82 | [166.4, 2.0, 1.0], 83 | [178.9, 5.0, 1.0], 84 | ], 85 | columns=["StartTime", "InjRate", "FracFlow"], 86 | ) 87 | 88 | # Schedule 89 | 90 | model1 = DisplacementModel1D2P( 91 | NX=50, 92 | core_length=3.9, # cm 93 | core_area=12.0, # cm2 94 | permeability=250.0, # mDarcy 95 | porosity=0.18, # v/v 96 | sw_initial=SWC, # v/v 97 | viscosity_w=1.0, # cP 98 | viscosity_n=2.0, # cP 99 | density_w=1000.0, # kg/m3 100 | density_n=800.0, # kg/m3 101 | rlp_model=rlp_model1, 102 | cpr_model=cpr_model1, 103 | time_end=T_END, # hour 104 | rate_schedule=Schedule, 105 | movie_schedule=Movie_times, 106 | ) 107 | 108 | results = model1.solve().results 109 | 110 | def wrapper(): 111 | model1.solve().results 112 | 113 | benchmark(wrapper) 114 | -------------------------------------------------------------------------------- /python/scores_benchmark_data/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /python/scores_benchmark_data/read_scores_data.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------ 2 | # Copyright (c) Shell Global Solutions International B.V. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | #------------------------------------------------------------------------------------------------ 5 | 6 | #-------------------------------------------------------------------------- 7 | """ 8 | Utility to extract parameters and results from a SCORES dataset stored 9 | in an Excel spreadsheet. Physical units are converted and data reformated 10 | so that data is ready for use with the 1D2P solver. 11 | 12 | 13 | Note: 14 | - this utility is constructed to be used in a jupyter notebook and makes use 15 | of IPython to display summary of data (function "print_summary"). 16 | - this utility is build to parse the four benchmark cases as provided 17 | in this directory. It may not be sufficient to read other SCORES data files. 18 | 19 | """ 20 | 21 | import os 22 | import numpy as np 23 | import pandas as pd 24 | 25 | 26 | class dictn(dict): 27 | """A dictionary making use of attributes to acces data.""" 28 | def __getattr__(self,k): 29 | return self[k] 30 | def __setattr__(self,k,v): 31 | self[k] = v 32 | 33 | def row_as_float(row): 34 | def try_float(v): 35 | try: 36 | f = float(v) 37 | except: 38 | f = np.nan 39 | return f 40 | return np.array([try_float(v) for v in row]) 41 | 42 | def read_table(df,i,col_names): 43 | num_cols = len(col_names) 44 | data = [] 45 | i += 1 46 | l = row_as_float(df.values[i][:num_cols]) 47 | while(i