pyrestoolbox 3.1.2__tar.gz → 3.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/PKG-INFO +1 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/build_pure_python.py +22 -4
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyproject.toml +1 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/_accelerator.py +43 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/_lib_vle_engine.py +44 -6
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/brine.py +151 -103
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/classes/classes.py +1 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/dca/dca.py +11 -14
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/changelist.rst +23 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/gas/gas.py +283 -189
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/matbal/matbal.py +24 -37
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/nodal/nodal.py +607 -939
- pyrestoolbox-3.1.4/pyrestoolbox/oil/__init__.py +76 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_compressibility.py +375 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_constants.py +125 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_correlations.py +490 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_density.py +227 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_harmonize.py +139 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_pvt_class.py +172 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_rate.py +192 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_separator.py +111 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_tables.py +272 -0
- pyrestoolbox-3.1.4/pyrestoolbox/oil/_utils.py +226 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/iapws_if97.py +9 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/plyasunov_model.py +12 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/simtools/simtools.py +1 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/setup.cfg +1 -1
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/setup.py +6 -2
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/gas_viscosity/mod.rs +51 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/lib.rs +12 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/pseudopressure.rs +161 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/flash.rs +13 -5
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/segment_gas.rs +16 -40
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/segment_oil.rs +12 -36
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/zfactor/mod.rs +74 -0
- pyrestoolbox-3.1.2/pyrestoolbox/oil/__init__.py +0 -1
- pyrestoolbox-3.1.2/pyrestoolbox/oil/oil.py +0 -2146
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/.gitignore +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/Cargo.lock +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/Cargo.toml +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/LICENSE +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/MANIFEST.in +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/README.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/benchmark_rust_vs_python.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/brine.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/examples.ipynb +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/gas.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/matbal.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/oil.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/recommend.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/recommend/recommend.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/pyrestoolbox/validate/validate.py +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/bessel.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/critical_properties/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/gwr.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/oil/density.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/oil/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/spycher_pruess/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/spycher_pruess/solubility.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/bip.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/components.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/k_init.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/holdup_hb.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/pvt_helpers.rs +0 -0
- {pyrestoolbox-3.1.2 → pyrestoolbox-3.1.4}/src/vlp/static_column.rs +0 -0
|
@@ -19,17 +19,35 @@ ROOT = Path(__file__).parent
|
|
|
19
19
|
|
|
20
20
|
def main():
|
|
21
21
|
# Write a temporary pyproject.toml for setuptools (pure Python)
|
|
22
|
+
# Strategy: keep the full [project] section from the real pyproject.toml
|
|
23
|
+
# (so modern setuptools gets name, version, dependencies, etc.) but swap
|
|
24
|
+
# [build-system] from maturin to setuptools.
|
|
25
|
+
import re
|
|
22
26
|
with tempfile.TemporaryDirectory() as tmp:
|
|
23
27
|
tmp = Path(tmp)
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
real_content = (ROOT / "pyproject.toml").read_text()
|
|
30
|
+
|
|
31
|
+
# Replace [build-system] section (everything up to next [section] or EOF)
|
|
32
|
+
pp_content = re.sub(
|
|
33
|
+
r'^\[build-system\].*?(?=^\[(?!build-system)|\Z)',
|
|
28
34
|
'[build-system]\n'
|
|
29
35
|
'requires = ["setuptools", "wheel"]\n'
|
|
30
|
-
'build-backend = "setuptools.build_meta"\n'
|
|
36
|
+
'build-backend = "setuptools.build_meta"\n\n',
|
|
37
|
+
real_content,
|
|
38
|
+
flags=re.MULTILINE | re.DOTALL,
|
|
39
|
+
)
|
|
40
|
+
# Remove [tool.maturin] section (not relevant for setuptools)
|
|
41
|
+
pp_content = re.sub(
|
|
42
|
+
r'^\[tool\.maturin\].*?(?=^\[(?!tool\.maturin)|\Z)',
|
|
43
|
+
'',
|
|
44
|
+
pp_content,
|
|
45
|
+
flags=re.MULTILINE | re.DOTALL,
|
|
31
46
|
)
|
|
32
47
|
|
|
48
|
+
pp_toml = tmp / "pyproject.toml"
|
|
49
|
+
pp_toml.write_text(pp_content)
|
|
50
|
+
|
|
33
51
|
# Build using setup.py + the temporary pyproject.toml
|
|
34
52
|
# We swap pyproject.toml temporarily
|
|
35
53
|
real_toml = ROOT / "pyproject.toml"
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyrestoolbox"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.4"
|
|
8
8
|
description = "pyResToolbox - A collection of Reservoir Engineering Utilities"
|
|
9
9
|
license = {text = "GPL-3.0-or-later"}
|
|
10
10
|
authors = [{name = "Mark W. Burgoyne", email = "mark.w.burgoyne@gmail.com"}]
|
|
@@ -156,13 +156,23 @@ else:
|
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def get_status():
|
|
159
|
-
|
|
159
|
+
status = {
|
|
160
160
|
"rust_available": RUST_AVAILABLE,
|
|
161
161
|
"failure_reason": _failure_reason if not RUST_AVAILABLE else "",
|
|
162
162
|
"forced_python": _force_python,
|
|
163
163
|
"sentinel_path": str(_sentinel_path()),
|
|
164
164
|
"sentinel_exists": _sentinel_path().exists(),
|
|
165
165
|
}
|
|
166
|
+
if RUST_AVAILABLE:
|
|
167
|
+
status["rust_version"] = get_rust_version()
|
|
168
|
+
return status
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_rust_version():
|
|
172
|
+
"""Return Rust extension version string, or None if unavailable."""
|
|
173
|
+
if not RUST_AVAILABLE or _rust_module is None:
|
|
174
|
+
return None
|
|
175
|
+
return getattr(_rust_module, '__version__', getattr(_rust_module, 'version', None))
|
|
166
176
|
|
|
167
177
|
|
|
168
178
|
def clear_block():
|
|
@@ -172,3 +182,35 @@ def clear_block():
|
|
|
172
182
|
"note": "Restart Python process to retry Rust extension loading",
|
|
173
183
|
**get_status(),
|
|
174
184
|
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def rust_accelerated(rust_fn_name):
|
|
188
|
+
"""Decorator that dispatches to a Rust implementation when available.
|
|
189
|
+
|
|
190
|
+
The decorated function is the pure-Python fallback. When Rust is available,
|
|
191
|
+
the decorator calls ``_rust_module.<rust_fn_name>`` with the same positional
|
|
192
|
+
and keyword arguments. On ImportError or AttributeError (missing function),
|
|
193
|
+
it falls back to the Python implementation transparently.
|
|
194
|
+
|
|
195
|
+
Usage::
|
|
196
|
+
|
|
197
|
+
@rust_accelerated('hb_fbhp_gas_rust')
|
|
198
|
+
def _hb_fbhp_gas(thp, api, gsg, ...):
|
|
199
|
+
return _segment_march_gas(...) # pure-Python path
|
|
200
|
+
"""
|
|
201
|
+
import functools
|
|
202
|
+
|
|
203
|
+
def decorator(fn):
|
|
204
|
+
@functools.wraps(fn)
|
|
205
|
+
def wrapper(*args, **kwargs):
|
|
206
|
+
if RUST_AVAILABLE:
|
|
207
|
+
try:
|
|
208
|
+
rust_fn = getattr(_rust_module, rust_fn_name)
|
|
209
|
+
return rust_fn(*args, **kwargs)
|
|
210
|
+
except (ImportError, AttributeError):
|
|
211
|
+
pass
|
|
212
|
+
return fn(*args, **kwargs)
|
|
213
|
+
wrapper._rust_fn_name = rust_fn_name
|
|
214
|
+
wrapper._python_fn = fn
|
|
215
|
+
return wrapper
|
|
216
|
+
return decorator
|
|
@@ -46,6 +46,7 @@ Author: Mark Burgoyne, Markus H. Nielsen
|
|
|
46
46
|
Date: 2025-2026
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
+
import warnings
|
|
49
50
|
import numpy as np
|
|
50
51
|
from typing import Dict, Tuple, Optional, List, Callable
|
|
51
52
|
from dataclasses import dataclass
|
|
@@ -1081,6 +1082,11 @@ def rr_solver(
|
|
|
1081
1082
|
b = (b_min + b_max) / 2.0
|
|
1082
1083
|
|
|
1083
1084
|
if N_it > max_iter:
|
|
1085
|
+
warnings.warn(
|
|
1086
|
+
f"Rachford-Rice solver did not converge in {max_iter} iterations "
|
|
1087
|
+
f"(residual={abs(h_b):.2e}, tol={tol:.1e}). Results may be inaccurate.",
|
|
1088
|
+
RuntimeWarning, stacklevel=3
|
|
1089
|
+
)
|
|
1084
1090
|
break
|
|
1085
1091
|
|
|
1086
1092
|
# Recover compositions from transformed variables
|
|
@@ -1590,6 +1596,8 @@ class SWBinaryVLE:
|
|
|
1590
1596
|
x = np.array([0.999, 0.001])
|
|
1591
1597
|
y = np.array([0.02, 0.98])
|
|
1592
1598
|
|
|
1599
|
+
damp = 0.3
|
|
1600
|
+
prev_error = np.inf
|
|
1593
1601
|
for iteration in range(max_iter):
|
|
1594
1602
|
x = np.clip(x, 1e-14, 1.0 - 1e-14)
|
|
1595
1603
|
x = x / np.sum(x)
|
|
@@ -1606,7 +1614,13 @@ class SWBinaryVLE:
|
|
|
1606
1614
|
if error < 1e-10:
|
|
1607
1615
|
return y[0]
|
|
1608
1616
|
|
|
1609
|
-
|
|
1617
|
+
# Adaptive damping
|
|
1618
|
+
if error < prev_error:
|
|
1619
|
+
damp = min(damp * 1.15, 0.8)
|
|
1620
|
+
else:
|
|
1621
|
+
damp = max(damp * 0.5, 0.1)
|
|
1622
|
+
prev_error = error
|
|
1623
|
+
|
|
1610
1624
|
y = y + damp * (y_new - y)
|
|
1611
1625
|
y = y / np.sum(y)
|
|
1612
1626
|
|
|
@@ -1895,7 +1909,20 @@ class SWMultiComponentFlash:
|
|
|
1895
1909
|
return ai, bi
|
|
1896
1910
|
|
|
1897
1911
|
def build_kij_matrix(self, T_K: float, mode: str = 'AQ') -> np.ndarray:
|
|
1898
|
-
"""Build N×N kij matrix. mode='AQ' or 'NA' for gas-water pairs.
|
|
1912
|
+
"""Build N×N kij matrix. mode='AQ' or 'NA' for gas-water pairs.
|
|
1913
|
+
|
|
1914
|
+
Results are cached per (T_K, mode) since the matrix only depends on
|
|
1915
|
+
temperature, mode, and instance-level constants (salinity, framework,
|
|
1916
|
+
component list) that don't change between calls.
|
|
1917
|
+
"""
|
|
1918
|
+
cache_key = (T_K, mode)
|
|
1919
|
+
if hasattr(self, '_kij_cache'):
|
|
1920
|
+
cached = self._kij_cache.get(cache_key)
|
|
1921
|
+
if cached is not None:
|
|
1922
|
+
return cached.copy()
|
|
1923
|
+
else:
|
|
1924
|
+
self._kij_cache = {}
|
|
1925
|
+
|
|
1899
1926
|
kij = np.zeros((self.nc, self.nc))
|
|
1900
1927
|
for i in range(self.nc):
|
|
1901
1928
|
for j in range(i + 1, self.nc):
|
|
@@ -1911,6 +1938,8 @@ class SWMultiComponentFlash:
|
|
|
1911
1938
|
val = get_gas_gas_bip(ni, nj)
|
|
1912
1939
|
kij[i, j] = val
|
|
1913
1940
|
kij[j, i] = val
|
|
1941
|
+
|
|
1942
|
+
self._kij_cache[cache_key] = kij.copy()
|
|
1914
1943
|
return kij
|
|
1915
1944
|
|
|
1916
1945
|
def calc_fugacity_coefficients(self, T_K: float, P_Pa: float, comp: np.ndarray,
|
|
@@ -2083,7 +2112,7 @@ class SWMultiComponentFlash:
|
|
|
2083
2112
|
0.0, mode, gamma_arr.tolist(),
|
|
2084
2113
|
)
|
|
2085
2114
|
return V, np.array(x_r), np.array(y_r), True
|
|
2086
|
-
except
|
|
2115
|
+
except (ImportError, AttributeError):
|
|
2087
2116
|
pass
|
|
2088
2117
|
|
|
2089
2118
|
kij_matrix = self.build_kij_matrix(T_K, mode)
|
|
@@ -2107,6 +2136,8 @@ class SWMultiComponentFlash:
|
|
|
2107
2136
|
K[self.iw] = min(K[self.iw], 0.01)
|
|
2108
2137
|
|
|
2109
2138
|
converged = False
|
|
2139
|
+
damp = 0.5
|
|
2140
|
+
prev_err = np.inf
|
|
2110
2141
|
for it in range(max_iter):
|
|
2111
2142
|
# Robust RR solver (Nielsen & Lia 2022)
|
|
2112
2143
|
V, x, y = solve_rachford_rice(z, K)
|
|
@@ -2121,12 +2152,19 @@ class SWMultiComponentFlash:
|
|
|
2121
2152
|
# Gamma-phi K-value: K_i = γ_i × φ_i^L / φ_i^V
|
|
2122
2153
|
K_new = np.clip(gamma_eff * phi_L / (phi_V + 1e-30), 1e-10, 1e10)
|
|
2123
2154
|
|
|
2124
|
-
|
|
2155
|
+
err = np.max(np.abs(K_new / K - 1.0))
|
|
2156
|
+
if err < tol:
|
|
2125
2157
|
converged = True
|
|
2126
2158
|
K = K_new
|
|
2127
2159
|
break
|
|
2128
2160
|
|
|
2129
|
-
|
|
2161
|
+
# Adaptive damping: accelerate when converging, brake when stalling
|
|
2162
|
+
if err < prev_err:
|
|
2163
|
+
damp = min(damp * 1.1, 0.95)
|
|
2164
|
+
else:
|
|
2165
|
+
damp = max(damp * 0.5, 0.1)
|
|
2166
|
+
prev_err = err
|
|
2167
|
+
|
|
2130
2168
|
K = K * (K_new / K)**damp
|
|
2131
2169
|
|
|
2132
2170
|
# Final compositions with converged K
|
|
@@ -2263,7 +2301,7 @@ class SWMultiComponentFlash:
|
|
|
2263
2301
|
if gamma_aq is not None:
|
|
2264
2302
|
result['gamma'] = gamma_aq
|
|
2265
2303
|
return result
|
|
2266
|
-
except
|
|
2304
|
+
except (ImportError, AttributeError):
|
|
2267
2305
|
pass
|
|
2268
2306
|
|
|
2269
2307
|
# Calculate gamma for gamma-phi method
|
|
@@ -46,7 +46,7 @@ from tabulate import tabulate
|
|
|
46
46
|
|
|
47
47
|
import pyrestoolbox.gas as gas # Needed for Z-Factor
|
|
48
48
|
from pyrestoolbox.classes import z_method, c_method, pb_method, rs_method, bo_method, uo_method, deno_method, co_method, kr_family, kr_table, class_dic
|
|
49
|
-
from pyrestoolbox.shared_fns import convert_to_numpy, process_output, halley_solve_cubic
|
|
49
|
+
from pyrestoolbox.shared_fns import convert_to_numpy, process_output, halley_solve_cubic, validate_pe_inputs
|
|
50
50
|
from pyrestoolbox.validate import validate_methods
|
|
51
51
|
from pyrestoolbox.constants import (R, psc, tsc, degF2R, tscr, scf_per_mol, CUFTperBBL, WDEN, MW_CO2, MW_H2S, MW_N2, MW_AIR, MW_H2,
|
|
52
52
|
BAR_TO_PSI, PSI_TO_BAR, degc_to_degf, degf_to_degc,
|
|
@@ -76,20 +76,71 @@ _FM32T_ARR = [0, -0.617, -0.747, -0.4339, 0, 10.26]
|
|
|
76
76
|
_FM1T_ARR = [0, 0, 9.917, 5.1128, 0, 3.892]
|
|
77
77
|
_FM12T_ARR = [0, 0.0365, -0.0369, 0, 0, 0]
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
# IAPWS-IF97 vapor pressure coefficients (Wagner & Pruss, 2002)
|
|
80
|
+
_IAPWS_TC_K = 647.096 # Critical temperature of water (K)
|
|
81
|
+
_IAPWS_PC_MPA = 22.064 # Critical pressure of water (MPa)
|
|
82
|
+
_IAPWS_VAP_A = [0, -7.85951783, 1.84408259, -11.7866497, 22.6807411, -15.9618719, 1.80122502]
|
|
83
|
+
|
|
84
|
+
# Duan-Mao CH4 solubility coefficients (McCain Table 4-15/4-16)
|
|
85
|
+
_DUAN_A = [0, 0, -0.004462, -0.06763, 0, 0]
|
|
86
|
+
_DUAN_B = [0, -0.03602, 0.18917, 0.97242, 0, 0]
|
|
87
|
+
_DUAN_C = [0, 0.6855, -3.1992, -3.7968, 0.07711, 0.2229]
|
|
88
|
+
_DUAN_U = [0, 8.3143711, -7.2772168e-4, 2.1489858e3, -1.4019672e-5, -6.6743449e5,
|
|
89
|
+
7.698589e-2, -5.0253331e-5, -30.092013, 4.8468502e3, 0]
|
|
90
|
+
_DUAN_LAMBDA = [0, -0.80898, 1.0827e-3, 183.85, 0, 0,
|
|
91
|
+
3.924e-4, 0, 0, 0, -1.97e-6]
|
|
92
|
+
_DUAN_ETA = [0, -3.89e-3, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
93
|
+
|
|
94
|
+
# Mao-Duan (2009) viscosity coefficients (McCain Table 4-14)
|
|
95
|
+
_MAODUAN_D = [0, 2885310, -11072.577, -9.0834095, 0.030925651, -0.0000274071,
|
|
96
|
+
-1928385.1, 5621.6046, 13.82725, -0.047609523, 0.000035545041]
|
|
97
|
+
_MAODUAN_A = [-0.21319213, 0.0013651589, -0.0000012191756]
|
|
98
|
+
_MAODUAN_B = [0.069161945, -0.00027292263, 0.0000002085244]
|
|
99
|
+
_MAODUAN_C = [-0.0025988855, 0.0000077989227]
|
|
100
|
+
|
|
101
|
+
# Gas constant in cm3·MPa/(mol·K)
|
|
102
|
+
_R_CM3_MPA = 8.314467
|
|
103
|
+
|
|
104
|
+
# Garcia (2001) partial molar volume of dissolved CO2 (Eq. 3)
|
|
105
|
+
_GARCIA_VMV = [37.51, -0.09585, 0.000874, -0.0000005044]
|
|
106
|
+
|
|
107
|
+
# Unit conversions
|
|
108
|
+
_MPA_TO_PSI = 145.038 # MPa -> psi
|
|
109
|
+
_SM3_TO_SCFSTB = 0.1781076 # sm3/sm3 -> scf/stb
|
|
110
|
+
|
|
111
|
+
# Standard temperature in K (60 degF)
|
|
112
|
+
_T_SC_K = (60 - 32) / 1.8 + 273.15 # 288.7056 K
|
|
113
|
+
|
|
114
|
+
# Methane specific gravity
|
|
115
|
+
_SG_METHANE = 0.5537
|
|
116
|
+
|
|
117
|
+
# Spycher-Pruess K-value coefficients (low-temperature, non-saturated CO2)
|
|
118
|
+
_SP_K_CO2_LT = [1.189, 1.304e-2, -5.446e-5]
|
|
119
|
+
_SP_K_H2O_LT = [-2.209, 3.097e-2, -1.098e-4, 2.048e-7]
|
|
120
|
+
|
|
121
|
+
def brine_props(p: float = None, degf: float = None, wt: float = None, ch4_sat: float=0,
|
|
122
|
+
metric: bool = False, *, pres: float = None, temp: float = None, ppm: float = None) -> Tuple:
|
|
80
123
|
""" Calculates Brine properties from modified Spivey Correlation per McCain Petroleum Reservoir Fluid Properties pg 160
|
|
81
124
|
Returns Tuple of (Bw (rb/stb | rm3/sm3), Density (sg), viscosity (cP), Compressibility (1/psi | 1/bar), Rw GOR (scf/stb | sm3/sm3))
|
|
82
|
-
p: Pressure (psia | barsa)
|
|
83
|
-
degf: Temperature (deg F | deg C)
|
|
84
|
-
wt: Salt wt% (0-100)
|
|
125
|
+
p: Pressure (psia | barsa). Alias: pres
|
|
126
|
+
degf: Temperature (deg F | deg C). Alias: temp
|
|
127
|
+
wt: Salt wt% (0-100). Alias: ppm (auto-converted: wt = ppm / 10000)
|
|
85
128
|
ch4_sat: Degree of methane saturation (0 - 1)
|
|
86
129
|
metric: If True, inputs/outputs in Eclipse METRIC units (barsa, degC, 1/bar, sm3/sm3). Default False (oilfield).
|
|
87
130
|
"""
|
|
131
|
+
# Resolve parameter aliases
|
|
132
|
+
if p is None and pres is not None:
|
|
133
|
+
p = pres
|
|
134
|
+
if degf is None and temp is not None:
|
|
135
|
+
degf = temp
|
|
136
|
+
if wt is None:
|
|
137
|
+
wt = ppm / 10000 if ppm is not None else 0
|
|
138
|
+
if p is None or degf is None:
|
|
139
|
+
raise TypeError("brine_props() requires pressure (p or pres) and temperature (degf or temp)")
|
|
88
140
|
if metric:
|
|
89
141
|
p = p * BAR_TO_PSI
|
|
90
142
|
degf = degc_to_degf(degf)
|
|
91
|
-
|
|
92
|
-
raise ValueError("Pressure must be positive")
|
|
143
|
+
validate_pe_inputs(p=p, degf=degf)
|
|
93
144
|
if wt < 0 or wt >= 100:
|
|
94
145
|
raise ValueError(f"Salt weight percent must be >= 0 and < 100, got {wt}")
|
|
95
146
|
|
|
@@ -188,17 +239,9 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
188
239
|
salt_ratio_sc = Rhob_scm_spivey / rhow_sc_spivey if m > 0 else 1.0
|
|
189
240
|
Rhob_scm = rhow_sc * salt_ratio_sc
|
|
190
241
|
|
|
191
|
-
a_coefic =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
1.84408259,
|
|
195
|
-
-11.7866497,
|
|
196
|
-
22.6807411,
|
|
197
|
-
-15.9618719,
|
|
198
|
-
1.80122502,
|
|
199
|
-
]
|
|
200
|
-
x = 1 - (degk / 647.096) # Eq 4.14
|
|
201
|
-
ln_vap_ratio = (647.096 / degk) * (
|
|
242
|
+
a_coefic = _IAPWS_VAP_A
|
|
243
|
+
x = 1 - (degk / _IAPWS_TC_K) # Eq 4.14
|
|
244
|
+
ln_vap_ratio = (_IAPWS_TC_K / degk) * (
|
|
202
245
|
a_coefic[1] * x
|
|
203
246
|
+ a_coefic[2] * x ** 1.5
|
|
204
247
|
+ a_coefic[3] * np.power(x, 3)
|
|
@@ -206,11 +249,11 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
206
249
|
+ a_coefic[5] * np.power(x, 4)
|
|
207
250
|
+ a_coefic[6] * np.power(x, 7.5)
|
|
208
251
|
) # Eq 4.13
|
|
209
|
-
vap_pressure = np.exp(ln_vap_ratio) *
|
|
252
|
+
vap_pressure = np.exp(ln_vap_ratio) * _IAPWS_PC_MPA
|
|
210
253
|
|
|
211
|
-
a_coefic =
|
|
212
|
-
b_coefic =
|
|
213
|
-
c_coefic =
|
|
254
|
+
a_coefic = _DUAN_A
|
|
255
|
+
b_coefic = _DUAN_B
|
|
256
|
+
c_coefic = _DUAN_C
|
|
214
257
|
|
|
215
258
|
A_t = Eq41(degc, a_coefic)
|
|
216
259
|
B_t = Eq41(degc, b_coefic)
|
|
@@ -221,33 +264,9 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
221
264
|
except (ValueError, FloatingPointError):
|
|
222
265
|
mch4w = 0
|
|
223
266
|
|
|
224
|
-
u_arr =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
-7.2772168e-4,
|
|
228
|
-
2.1489858e3,
|
|
229
|
-
-1.4019672e-5,
|
|
230
|
-
-6.6743449e5,
|
|
231
|
-
7.698589e-2,
|
|
232
|
-
-5.0253331e-5,
|
|
233
|
-
-30.092013,
|
|
234
|
-
4.8468502e3,
|
|
235
|
-
0,
|
|
236
|
-
]
|
|
237
|
-
lambda_arr = [
|
|
238
|
-
0,
|
|
239
|
-
-0.80898,
|
|
240
|
-
1.0827e-3,
|
|
241
|
-
183.85,
|
|
242
|
-
0,
|
|
243
|
-
0,
|
|
244
|
-
3.924e-4,
|
|
245
|
-
0,
|
|
246
|
-
0,
|
|
247
|
-
0,
|
|
248
|
-
-1.97e-6,
|
|
249
|
-
]
|
|
250
|
-
eta_arr = [0, -3.89e-3, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
267
|
+
u_arr = _DUAN_U
|
|
268
|
+
lambda_arr = _DUAN_LAMBDA
|
|
269
|
+
eta_arr = _DUAN_ETA
|
|
251
270
|
|
|
252
271
|
lambda_ch4Na = (
|
|
253
272
|
lambda_arr[1]
|
|
@@ -273,7 +292,7 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
273
292
|
detadptm = 0 # Eq 4.21
|
|
274
293
|
|
|
275
294
|
Vmch4b = (
|
|
276
|
-
|
|
295
|
+
_R_CM3_MPA * degk * (dudptm + 2 * m * dlambdadptm + m * m * 0)
|
|
277
296
|
) # Eq 4.22
|
|
278
297
|
vb0 = 1 / Rhob_tpm # Eq 4.23
|
|
279
298
|
rhobtpbch4 = (1000 + m * 58.4428 + mch4 * 16.043) / (
|
|
@@ -285,7 +304,7 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
285
304
|
d2lambdadp2 = 2 * lambda_arr[10]
|
|
286
305
|
d2etadp2 = 0
|
|
287
306
|
dVmch4dp = (
|
|
288
|
-
|
|
307
|
+
_R_CM3_MPA * degk * (d2uch2dp2 + 2 * m * d2lambdadp2 + m * m * d2etadp2)
|
|
289
308
|
) # Eq 4.31
|
|
290
309
|
cwu = -((1000 + m * 58.4428) * dvbdp + mch4 * dVmch4dp) / (
|
|
291
310
|
(1000 + m * 58.4428) * vb0 + (mch4 * Vmch4b)
|
|
@@ -296,10 +315,10 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
296
315
|
/ ((Mpa - vap_pressure) - 2 * dlambdadptm * m)
|
|
297
316
|
) # Eq 4.33
|
|
298
317
|
|
|
299
|
-
zee = gas.gas_z(p=p, sg=
|
|
318
|
+
zee = gas.gas_z(p=p, sg=_SG_METHANE, degf=degf, zmethod='BNS',
|
|
300
319
|
co2=0, h2s=0, n2=0, h2=0) # Z-Factor of pure methane
|
|
301
320
|
|
|
302
|
-
vmch4g = zee *
|
|
321
|
+
vmch4g = zee * _R_CM3_MPA * degk / Mpa # Eq 4.34
|
|
303
322
|
|
|
304
323
|
cws = -(
|
|
305
324
|
(1000 + m * 58.4428) * dvbdp
|
|
@@ -308,7 +327,7 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
308
327
|
) / (
|
|
309
328
|
(1000 + m * 58.4428) * vb0 + (mch4 * Vmch4b)
|
|
310
329
|
) # Eq 4.35 - Compressibility of saturated brine Mpa-1
|
|
311
|
-
cw_new = 1 / (
|
|
330
|
+
cw_new = 1 / (_MPA_TO_PSI * (1 / cws)) # Compressibility in psi-1
|
|
312
331
|
vb0_sc = (
|
|
313
332
|
1 / Rhob_scm
|
|
314
333
|
) # vb0 at standard conditions - (Calculated by evaluating vbo at 0.1013 MPa and 15 degC)
|
|
@@ -323,28 +342,16 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
323
342
|
|
|
324
343
|
|
|
325
344
|
|
|
326
|
-
zee_sc = gas.gas_z(p=psc, sg=
|
|
345
|
+
zee_sc = gas.gas_z(p=psc, sg=_SG_METHANE, degf=tsc, zmethod='BNS',
|
|
327
346
|
co2=0, h2s=0, n2=0, h2=0)
|
|
328
|
-
vmch4g_sc = zee_sc *
|
|
347
|
+
vmch4g_sc = zee_sc * _R_CM3_MPA * (273 + 15) / 0.1013 # Eq 4.34
|
|
329
348
|
rsw_new = mch4 * vmch4g_sc / ((1000 + m * 58.4428) * vb0_sc)
|
|
330
|
-
rsw_new_oilfield = rsw_new /
|
|
331
|
-
|
|
332
|
-
d =
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
-9.0834095,
|
|
337
|
-
0.030925651,
|
|
338
|
-
-0.0000274071,
|
|
339
|
-
-1928385.1,
|
|
340
|
-
5621.6046,
|
|
341
|
-
13.82725,
|
|
342
|
-
-0.047609523,
|
|
343
|
-
0.000035545041,
|
|
344
|
-
]
|
|
345
|
-
a = [-0.21319213, 0.0013651589, -0.0000012191756]
|
|
346
|
-
b = [0.069161945, -0.00027292263, 0.0000002085244]
|
|
347
|
-
c = [-0.0025988855, 0.0000077989227]
|
|
349
|
+
rsw_new_oilfield = rsw_new / _SM3_TO_SCFSTB # Convert to scf/stb
|
|
350
|
+
|
|
351
|
+
d = _MAODUAN_D
|
|
352
|
+
a = _MAODUAN_A
|
|
353
|
+
b = _MAODUAN_B
|
|
354
|
+
c = _MAODUAN_C
|
|
348
355
|
|
|
349
356
|
lnuw_tp = sum([d[i] * np.power(degk, (i - 3)) for i in range(1, 6)])
|
|
350
357
|
lnuw_tp += sum(
|
|
@@ -364,7 +371,7 @@ def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bo
|
|
|
364
371
|
bw = Bw # rb/stb (dimensionless ratio, same in both unit systems)
|
|
365
372
|
lden = rhobtpbch4 # sg (g/cm3)
|
|
366
373
|
visw = ub_tpm # cP
|
|
367
|
-
cwu_psi = 1 / (
|
|
374
|
+
cwu_psi = 1 / (_MPA_TO_PSI * (1 / cwu)) # Undersaturated compressibility in psi-1
|
|
368
375
|
cws_psi = cw_new # Saturated compressibility in psi-1
|
|
369
376
|
rsw = rsw_new_oilfield # scf/stb
|
|
370
377
|
|
|
@@ -465,7 +472,21 @@ class CO2_Brine_Mixture():
|
|
|
465
472
|
|
|
466
473
|
|
|
467
474
|
"""
|
|
468
|
-
def __init__(self, pres, temp, ppm
|
|
475
|
+
def __init__(self, pres=None, temp=None, ppm=None, metric=True, cw_sat=False,
|
|
476
|
+
*, p=None, degf=None, wt=None):
|
|
477
|
+
# Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
|
|
478
|
+
if pres is None and p is not None:
|
|
479
|
+
pres = p
|
|
480
|
+
if temp is None and degf is not None:
|
|
481
|
+
temp = degf
|
|
482
|
+
if ppm is None:
|
|
483
|
+
ppm = wt * 10000 if wt is not None else 0
|
|
484
|
+
if pres is None or temp is None:
|
|
485
|
+
raise TypeError("CO2_Brine_Mixture() requires pressure (pres or p) and temperature (temp or degf)")
|
|
486
|
+
# Validate pressure and temperature (convert to oilfield units for validation)
|
|
487
|
+
_p_val = pres if not metric else pres * BAR2PSI
|
|
488
|
+
_t_val = temp if not metric else temp * 1.8 + 32
|
|
489
|
+
validate_pe_inputs(p=_p_val, degf=_t_val)
|
|
469
490
|
self.metric = metric # Units. FIELD or METRIC
|
|
470
491
|
self.ppm = ppm # Parts (by wt) NaCl added to 1E6 parts of water
|
|
471
492
|
self.tKel = None # Deg K
|
|
@@ -863,16 +884,16 @@ class CO2_Brine_Mixture():
|
|
|
863
884
|
# CO2 K-value at reservoir Pressure
|
|
864
885
|
#=======================================================================
|
|
865
886
|
if self.low_temp:
|
|
866
|
-
x =
|
|
887
|
+
x = _SP_K_CO2_LT
|
|
867
888
|
if self.CO2_sat:
|
|
868
|
-
x = [1.169, 1.368e-2, -5.380e-5] # Liquid CO2 below 31 deg C and above CO2 Psat
|
|
889
|
+
x = [1.169, 1.368e-2, -5.380e-5] # Liquid CO2 below 31 deg C and above CO2 Psat
|
|
869
890
|
else:
|
|
870
891
|
x = [1.668, 3.992e-3, -1.156e-5, 1.593e-9]
|
|
871
892
|
|
|
872
893
|
K0 = 10**self.FT(self.degC, x)
|
|
873
|
-
|
|
894
|
+
|
|
874
895
|
if self.scaled:
|
|
875
|
-
K0_lt = 10**self.FT(self.degC,
|
|
896
|
+
K0_lt = 10**self.FT(self.degC, _SP_K_CO2_LT)
|
|
876
897
|
K0 = self.blended_val(K0_lt, K0)
|
|
877
898
|
|
|
878
899
|
self.K[0] = self.Ktp(K0, self.vBar[0])
|
|
@@ -882,14 +903,14 @@ class CO2_Brine_Mixture():
|
|
|
882
903
|
# H2O K-value at reservoir pressure
|
|
883
904
|
#=======================================================================
|
|
884
905
|
if self.low_temp:
|
|
885
|
-
x =
|
|
906
|
+
x = _SP_K_H2O_LT
|
|
886
907
|
else:
|
|
887
908
|
x = [-2.1077, 2.8127e-2, -8.4298e-5, 1.4969e-7, -1.1812e-10]
|
|
888
|
-
|
|
909
|
+
|
|
889
910
|
K0 = 10**self.FT(self.degC, x)
|
|
890
|
-
|
|
911
|
+
|
|
891
912
|
if self.scaled:
|
|
892
|
-
K0_lt = 10**self.FT(self.degC,
|
|
913
|
+
K0_lt = 10**self.FT(self.degC, _SP_K_H2O_LT)
|
|
893
914
|
K0 = self.blended_val(K0_lt, K0)
|
|
894
915
|
|
|
895
916
|
self.K[1] = self.Ktp(K0, self.vBar[1])
|
|
@@ -956,7 +977,7 @@ class CO2_Brine_Mixture():
|
|
|
956
977
|
self.MolarVol = self.MwGas / max(rhogas, 1e-30)
|
|
957
978
|
self.pRT = self.pBar / (RGASCON * self.tKel)
|
|
958
979
|
return
|
|
959
|
-
except
|
|
980
|
+
except (ImportError, AttributeError):
|
|
960
981
|
pass
|
|
961
982
|
|
|
962
983
|
pBar = self.pBar
|
|
@@ -1056,7 +1077,7 @@ class CO2_Brine_Mixture():
|
|
|
1056
1077
|
self.x[0] = self.Bprime * (1.0 - self.y[1])
|
|
1057
1078
|
self.y[0] = 1.0 - self.y[1]
|
|
1058
1079
|
|
|
1059
|
-
mCO2 = self.x[0] * (CONMOLA + 2 * self.molaL) / (1 - self.x[0]) # Eq B-6
|
|
1080
|
+
mCO2 = self.x[0] * (CONMOLA + 2 * self.molaL) / max(1 - self.x[0], 1e-15) # Eq B-6
|
|
1060
1081
|
self.xSalt = 2.0 * self.molaL / (2.0 * self.molaL + CONMOLA + mCO2) # Eq B-3. The 2.0x is stoichiometric ions for NaCl
|
|
1061
1082
|
self.x[1] = 1.0 - self.x[0] - self.xSalt
|
|
1062
1083
|
self.x[1] = min(max(self.x[1], 0), 1)
|
|
@@ -1100,9 +1121,17 @@ class CO2_Brine_Mixture():
|
|
|
1100
1121
|
self.x[1] = 1.0 - self.x[0] - self.xSalt
|
|
1101
1122
|
|
|
1102
1123
|
err = abs(self.y[1]/yH2O_last-1)
|
|
1103
|
-
|
|
1124
|
+
|
|
1104
1125
|
iternum += 1
|
|
1105
|
-
|
|
1126
|
+
|
|
1127
|
+
if err > EPS:
|
|
1128
|
+
import warnings
|
|
1129
|
+
warnings.warn(
|
|
1130
|
+
f"Spycher CO2-brine iteration did not converge in {iternum} iterations "
|
|
1131
|
+
f"(relative error={err:.2e}). Results may be inaccurate.",
|
|
1132
|
+
RuntimeWarning, stacklevel=2
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1106
1135
|
#=======================================================================
|
|
1107
1136
|
# Re-Compute the CO2/H2O Gas Phase Density
|
|
1108
1137
|
#=======================================================================
|
|
@@ -1172,12 +1201,10 @@ class CO2_Brine_Mixture():
|
|
|
1172
1201
|
Fm12t_arr = _FM12T_ARR
|
|
1173
1202
|
|
|
1174
1203
|
# Table 4-14 Mao-Duan Coefficients
|
|
1175
|
-
d =
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
b = [0.069161945, -0.00027292263, 0.0000002085244]
|
|
1180
|
-
c = [-0.0025988855, 0.0000077989227]
|
|
1204
|
+
d = _MAODUAN_D
|
|
1205
|
+
a = _MAODUAN_A
|
|
1206
|
+
b = _MAODUAN_B
|
|
1207
|
+
c = _MAODUAN_C
|
|
1181
1208
|
|
|
1182
1209
|
# Density of pure water at the reference pressure of 70 MPa, ?w(T, 70 MPa), in g/cm3,
|
|
1183
1210
|
rhow_t70 = Eq41(degc, rhow_t70_arr) # Step 1
|
|
@@ -1235,7 +1262,7 @@ class CO2_Brine_Mixture():
|
|
|
1235
1262
|
def partMolVol(degK):
|
|
1236
1263
|
# Partial Molar Volume of dissolved CO2: Garcia Eq (3)
|
|
1237
1264
|
tC = degK - 273.15
|
|
1238
|
-
return
|
|
1265
|
+
return _GARCIA_VMV[0] + tC * (_GARCIA_VMV[1] + tC * (_GARCIA_VMV[2] + tC * _GARCIA_VMV[3]))
|
|
1239
1266
|
|
|
1240
1267
|
# -- Correcting brine density for dissolved CO2, JE Garcia, LBNL Report# 49023, Oct 2011, "Density of Aqueous Solutions of CO2"
|
|
1241
1268
|
def garciaDensity(rhoBRnoCO2, tKel, pBar, ppm, xCO2, MwB, MwG):
|
|
@@ -1288,7 +1315,7 @@ class CO2_Brine_Mixture():
|
|
|
1288
1315
|
Fm12t = Eq41(degc, Fm12t_arr)
|
|
1289
1316
|
|
|
1290
1317
|
# -- CO2-Free Brine & Freshwater Density at standard conditions (gm/cm3)
|
|
1291
|
-
tKel_sc =
|
|
1318
|
+
tKel_sc = _T_SC_K # 60 degF -> K
|
|
1292
1319
|
sg_SC_Brine, rhowSC = brine_denw(PSTND/10, tKel_local=tKel_sc)
|
|
1293
1320
|
|
|
1294
1321
|
# Calculate mass of 1 sm3 of brine without CO2
|
|
@@ -1449,14 +1476,25 @@ class SoreideWhitson:
|
|
|
1449
1476
|
Murphy, W.R. and Gaines, T.M. (1974), J. Chem. Eng. Data 19(4), 359-362.
|
|
1450
1477
|
"""
|
|
1451
1478
|
|
|
1452
|
-
def __init__(self, pres, temp, ppm=
|
|
1453
|
-
sg=0.65, metric=True, cw_sat=False):
|
|
1479
|
+
def __init__(self, pres=None, temp=None, ppm=None, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0,
|
|
1480
|
+
sg=0.65, metric=True, cw_sat=False, *, p=None, degf=None, wt=None):
|
|
1481
|
+
# Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
|
|
1482
|
+
if pres is None and p is not None:
|
|
1483
|
+
pres = p
|
|
1484
|
+
if temp is None and degf is not None:
|
|
1485
|
+
temp = degf
|
|
1486
|
+
if ppm is None:
|
|
1487
|
+
ppm = wt * 10000 if wt is not None else 0
|
|
1488
|
+
if pres is None or temp is None:
|
|
1489
|
+
raise TypeError("SoreideWhitson() requires pressure (pres or p) and temperature (temp or degf)")
|
|
1454
1490
|
if ppm < 0:
|
|
1455
1491
|
raise ValueError(f"ppm must be non-negative, got {ppm}")
|
|
1456
1492
|
if ppm >= 1e6:
|
|
1457
1493
|
raise ValueError(f"ppm must be less than 1,000,000, got {ppm}")
|
|
1458
|
-
|
|
1459
|
-
|
|
1494
|
+
# Validate pressure and temperature (convert to oilfield units for validation)
|
|
1495
|
+
_p_val = pres if not metric else pres * BAR2PSI
|
|
1496
|
+
_t_val = temp if not metric else temp * 1.8 + 32
|
|
1497
|
+
validate_pe_inputs(p=_p_val, degf=_t_val)
|
|
1460
1498
|
non_hc = y_CO2 + y_H2S + y_N2 + y_H2
|
|
1461
1499
|
if non_hc > 1.0:
|
|
1462
1500
|
raise ValueError(f"Sum of non-HC gas fractions ({non_hc}) exceeds 1.0")
|
|
@@ -1764,9 +1802,19 @@ class SoreideWhitson:
|
|
|
1764
1802
|
# rho = (1 + x2*M2/(M1*x1)) / (x2*V_phi/(M1*x1) + 1/rho1)
|
|
1765
1803
|
x1 = 1.0 - self.x_total
|
|
1766
1804
|
M1 = MWWAT
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1805
|
+
if x1 < 1e-6:
|
|
1806
|
+
# Near-pure gas phase: Garcia mixing rule breaks down
|
|
1807
|
+
import warnings
|
|
1808
|
+
warnings.warn(
|
|
1809
|
+
f"Gas mole fraction x_total={self.x_total:.4f} is too high for "
|
|
1810
|
+
"Garcia density mixing rule. Returning unsaturated brine density.",
|
|
1811
|
+
RuntimeWarning, stacklevel=2
|
|
1812
|
+
)
|
|
1813
|
+
rho_gas_brine_gcc = rho_brine_gcc
|
|
1814
|
+
else:
|
|
1815
|
+
numerator = 1.0 + self.x_total * mw_eff / (M1 * x1)
|
|
1816
|
+
denominator = self.x_total * vphi_eff / (M1 * x1) + 1.0 / rho_brine_gcc
|
|
1817
|
+
rho_gas_brine_gcc = numerator / denominator
|
|
1770
1818
|
else:
|
|
1771
1819
|
rho_gas_brine_gcc = rho_brine_gcc
|
|
1772
1820
|
|