├── .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 | -
8 | About The Project
9 |
10 | -
11 | Getting Started
12 |
16 |
17 | - Usage
18 | - Roadmap
19 | - Contributing
20 | - License
21 | - Contact
22 | - Acknowledgments
23 | - How to Cite
24 |
25 |
26 |
27 |
28 |
29 |
30 |
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 |
62 | - a 1D numerical flow solver (Darcy fractional flow solver with capillarity in 1D) and
63 | - an inverse modelling framework which is utilizing the optimization package called lmfit from Python
64 |
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