pyrestoolbox 3.1.3__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.
Files changed (124) hide show
  1. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/PKG-INFO +1 -1
  2. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/build_pure_python.py +22 -4
  3. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyproject.toml +1 -1
  4. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/_accelerator.py +43 -1
  5. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/_lib_vle_engine.py +44 -6
  6. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/brine.py +151 -103
  7. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/classes/classes.py +1 -1
  8. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/dca/dca.py +11 -14
  9. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/changelist.rst +15 -0
  10. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/gas/gas.py +266 -174
  11. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/matbal/matbal.py +24 -37
  12. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/nodal/nodal.py +607 -939
  13. pyrestoolbox-3.1.4/pyrestoolbox/oil/__init__.py +76 -0
  14. pyrestoolbox-3.1.4/pyrestoolbox/oil/_compressibility.py +375 -0
  15. pyrestoolbox-3.1.4/pyrestoolbox/oil/_constants.py +125 -0
  16. pyrestoolbox-3.1.4/pyrestoolbox/oil/_correlations.py +490 -0
  17. pyrestoolbox-3.1.4/pyrestoolbox/oil/_density.py +227 -0
  18. pyrestoolbox-3.1.4/pyrestoolbox/oil/_harmonize.py +139 -0
  19. pyrestoolbox-3.1.4/pyrestoolbox/oil/_pvt_class.py +172 -0
  20. pyrestoolbox-3.1.4/pyrestoolbox/oil/_rate.py +192 -0
  21. pyrestoolbox-3.1.4/pyrestoolbox/oil/_separator.py +111 -0
  22. pyrestoolbox-3.1.4/pyrestoolbox/oil/_tables.py +272 -0
  23. pyrestoolbox-3.1.4/pyrestoolbox/oil/_utils.py +226 -0
  24. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/iapws_if97.py +9 -0
  25. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/plyasunov_model.py +12 -1
  26. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/simtools/simtools.py +1 -1
  27. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/setup.cfg +1 -1
  28. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/setup.py +6 -2
  29. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/lib.rs +3 -0
  30. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/pseudopressure.rs +161 -0
  31. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/flash.rs +13 -5
  32. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/segment_gas.rs +16 -40
  33. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/segment_oil.rs +12 -36
  34. pyrestoolbox-3.1.3/pyrestoolbox/oil/__init__.py +0 -1
  35. pyrestoolbox-3.1.3/pyrestoolbox/oil/oil.py +0 -2146
  36. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/.github/workflows/build-wheels.yml +0 -0
  37. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/.gitignore +0 -0
  38. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/Cargo.lock +0 -0
  39. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/Cargo.toml +0 -0
  40. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/LICENSE +0 -0
  41. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/MANIFEST.in +0 -0
  42. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/README.rst +0 -0
  43. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/ResToolbox/privacy_policy.md +0 -0
  44. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/benchmark_rust_vs_python.py +0 -0
  45. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/__init__.py +0 -0
  46. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/__init__.py +0 -0
  47. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  48. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/classes/__init__.py +0 -0
  49. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/constants/__init__.py +0 -0
  50. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/constants/constants.py +0 -0
  51. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/dca/__init__.py +0 -0
  52. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/brine.rst +0 -0
  53. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/dca.rst +0 -0
  54. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/examples.ipynb +0 -0
  55. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/gas.rst +0 -0
  56. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot.png +0 -0
  57. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  58. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/bot_img.png +0 -0
  59. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  60. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  61. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/influence.png +0 -0
  62. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/properties_df.png +0 -0
  63. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/sgof.png +0 -0
  64. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/img/swof.png +0 -0
  65. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/layer.rst +0 -0
  66. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/library.rst +0 -0
  67. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/matbal.rst +0 -0
  68. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal.rst +0 -0
  69. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
  70. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  71. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/oil.rst +0 -0
  72. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/recommend.rst +0 -0
  73. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/sensitivity.rst +0 -0
  74. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/docs/simtools.rst +0 -0
  75. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/gas/__init__.py +0 -0
  76. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/layer/__init__.py +0 -0
  77. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/layer/layer.py +0 -0
  78. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/library/__init__.py +0 -0
  79. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/library/component_library.xlsx +0 -0
  80. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/library/library.py +0 -0
  81. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/matbal/__init__.py +0 -0
  82. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/nodal/__init__.py +0 -0
  83. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/__init__.py +0 -0
  84. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  85. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/recommend/__init__.py +0 -0
  86. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/recommend/recommend.py +0 -0
  87. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/sensitivity/__init__.py +0 -0
  88. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  89. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/shared_fns/__init__.py +0 -0
  90. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  91. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/simtools/__init__.py +0 -0
  92. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/validate/__init__.py +0 -0
  93. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/pyrestoolbox/validate/validate.py +0 -0
  94. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/bessel.rs +0 -0
  95. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/critical_properties/mod.rs +0 -0
  96. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/dca/hyperbolic.rs +0 -0
  97. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/dca/mod.rs +0 -0
  98. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/dca/ransac.rs +0 -0
  99. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/gas_viscosity/mod.rs +0 -0
  100. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/gwr.rs +0 -0
  101. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/matbal/mod.rs +0 -0
  102. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/matbal/objective.rs +0 -0
  103. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/oil/density.rs +0 -0
  104. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/oil/mod.rs +0 -0
  105. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/spycher_pruess/mod.rs +0 -0
  106. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/spycher_pruess/solubility.rs +0 -0
  107. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/alpha.rs +0 -0
  108. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/bip.rs +0 -0
  109. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/components.rs +0 -0
  110. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/eos.rs +0 -0
  111. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/fugacity.rs +0 -0
  112. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/k_init.rs +0 -0
  113. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/mod.rs +0 -0
  114. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vle/rachford_rice.rs +0 -0
  115. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/friction.rs +0 -0
  116. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/holdup_bb.rs +0 -0
  117. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/holdup_gray.rs +0 -0
  118. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/holdup_hb.rs +0 -0
  119. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/holdup_wg.rs +0 -0
  120. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/ift.rs +0 -0
  121. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/mod.rs +0 -0
  122. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/pvt_helpers.rs +0 -0
  123. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/vlp/static_column.rs +0 -0
  124. {pyrestoolbox-3.1.3 → pyrestoolbox-3.1.4}/src/zfactor/mod.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 3.1.3
3
+ Version: 3.1.4
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -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
- # Copy the pure-Python pyproject.toml
26
- pp_toml = tmp / "pyproject.toml"
27
- pp_toml.write_text(
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.3"
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
- return {
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
- damp = 0.4
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 Exception:
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
- if np.max(np.abs(K_new / K - 1.0)) < tol:
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
- damp = 0.7 if it < 20 else 0.9
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 Exception:
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
- def brine_props(p: float, degf: float, wt: float=0, ch4_sat: float=0, metric: bool = False) -> Tuple:
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
- if p <= 0:
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
- 0,
193
- -7.85951783,
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) * 22.064
252
+ vap_pressure = np.exp(ln_vap_ratio) * _IAPWS_PC_MPA
210
253
 
211
- a_coefic = [0, 0, -0.004462, -0.06763, 0, 0]
212
- b_coefic = [0, -0.03602, 0.18917, 0.97242, 0, 0]
213
- c_coefic = [0, 0.6855, -3.1992, -3.7968, 0.07711, 0.2229]
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
- 0,
226
- 8.3143711,
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
- 8.314467 * degk * (dudptm + 2 * m * dlambdadptm + m * m * 0)
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
- 8.314467 * degk * (d2uch2dp2 + 2 * m * d2lambdadp2 + m * m * d2etadp2)
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=0.5537, degf=degf, zmethod='BNS',
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 * 8.314467 * degk / Mpa # Eq 4.34
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 / (145.038 * (1 / cws)) # Compressibility in psi-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=0.5537, degf=tsc, zmethod='BNS',
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 * 8.314467 * (273 + 15) / 0.1013 # Eq 4.34
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 / 0.1781076 # Convert to scf/stb
331
-
332
- d = [
333
- 0,
334
- 2885310,
335
- -11072.577,
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 / (145.038 * (1 / cwu)) # Undersaturated compressibility in 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 = 0, metric = True, cw_sat = False):
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 = [1.189, 1.304e-2, -5.446e-5]
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, [1.189, 1.304e-2, -5.446e-5])
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 = [-2.209, 3.097e-2, -1.098e-4, 2.048e-7]
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, [-2.209, 3.097e-2, -1.098e-4, 2.048e-7])
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 Exception:
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 = [0, 2885310, -11072.577, -9.0834095, 0.030925651, -0.0000274071, -1928385.1, 5621.6046, 13.82725, -0.047609523, 0.000035545041]
1176
-
1177
- # Table 4-14 Mao-Duan Coefficients
1178
- a = [-0.21319213, 0.0013651589, -0.0000012191756]
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 37.51 + tC * (-0.09585 + tC * (0.000874 - tC * 0.0000005044))
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 = (60 - 32) / 1.8 + 273.15 # 60 degF -> K
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=0, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0,
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
- if pres <= 0:
1459
- raise ValueError("Pressure must be positive")
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
- numerator = 1.0 + self.x_total * mw_eff / (M1 * x1)
1768
- denominator = self.x_total * vphi_eff / (M1 * x1) + 1.0 / rho_brine_gcc
1769
- rho_gas_brine_gcc = numerator / denominator
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