pyrestoolbox 3.8.0__tar.gz → 3.8.2__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 (152) hide show
  1. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/Cargo.lock +1 -1
  2. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/Cargo.toml +1 -1
  3. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/PKG-INFO +1 -1
  4. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyproject.toml +1 -1
  5. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/_lib_vle_engine.py +133 -9
  6. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/changelist.rst +8 -0
  7. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/nodal/nodal.py +10 -1
  8. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/flash.rs +178 -9
  9. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/march.rs +4 -1
  10. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/.github/complexity-baseline.json +0 -0
  11. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/.github/scripts/check_complexity.py +0 -0
  12. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/.github/workflows/build-wheels.yml +0 -0
  13. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/.github/workflows/tests.yml +0 -0
  14. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/.gitignore +0 -0
  15. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/LICENSE +0 -0
  16. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/MANIFEST.in +0 -0
  17. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/README.rst +0 -0
  18. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/ResToolbox/privacy_policy.md +0 -0
  19. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/build_pure_python.py +0 -0
  20. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/clippy.toml +0 -0
  21. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/Dockerfile +0 -0
  22. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/README.md +0 -0
  23. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/pyproject.toml +0 -0
  24. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/pyrestoolbox_mcp/__init__.py +0 -0
  25. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/pyrestoolbox_mcp/serialize.py +0 -0
  26. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/pyrestoolbox_mcp/server.py +0 -0
  27. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/mcp/tests/test_server.py +0 -0
  28. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/__init__.py +0 -0
  29. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/_accelerator.py +0 -0
  30. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/__init__.py +0 -0
  31. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/_lib_dielectric.py +0 -0
  32. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/_lib_maoduan_viscosity.py +0 -0
  33. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/_lib_pitzer_params.py +0 -0
  34. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  35. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/appelo_volumes.py +0 -0
  36. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/brine.py +0 -0
  37. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/iapws_viscosity.py +0 -0
  38. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/jones_dole_viscosity.py +0 -0
  39. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/kestin_nacl_viscosity.py +0 -0
  40. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/pr_vphi.py +0 -0
  41. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/viscosity_route.py +0 -0
  42. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/brine/vphi_route.py +0 -0
  43. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/classes/__init__.py +0 -0
  44. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/classes/classes.py +0 -0
  45. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/constants/__init__.py +0 -0
  46. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/constants/constants.py +0 -0
  47. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/dca/__init__.py +0 -0
  48. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/dca/dca.py +0 -0
  49. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/brine.rst +0 -0
  50. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/dca.rst +0 -0
  51. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/examples.ipynb +0 -0
  52. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/gas.rst +0 -0
  53. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/bot.png +0 -0
  54. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  55. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/bot_img.png +0 -0
  56. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  57. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  58. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/influence.png +0 -0
  59. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/properties_df.png +0 -0
  60. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/sgof.png +0 -0
  61. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/img/swof.png +0 -0
  62. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/index.rst +0 -0
  63. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/layer.rst +0 -0
  64. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/library.rst +0 -0
  65. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/matbal.rst +0 -0
  66. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/nodal.rst +0 -0
  67. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
  68. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  69. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/oil.rst +0 -0
  70. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/plyasunov.rst +0 -0
  71. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/recommend.rst +0 -0
  72. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/sensitivity.rst +0 -0
  73. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/docs/simtools.rst +0 -0
  74. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/gas/__init__.py +0 -0
  75. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/gas/_hydrate.py +0 -0
  76. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/gas/gas.py +0 -0
  77. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/layer/__init__.py +0 -0
  78. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/layer/layer.py +0 -0
  79. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/library/__init__.py +0 -0
  80. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/library/component_library.xlsx +0 -0
  81. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/library/library.py +0 -0
  82. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/matbal/__init__.py +0 -0
  83. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/matbal/matbal.py +0 -0
  84. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/nodal/__init__.py +0 -0
  85. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/__init__.py +0 -0
  86. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_compressibility.py +0 -0
  87. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_constants.py +0 -0
  88. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_correlations.py +0 -0
  89. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_density.py +0 -0
  90. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_harmonize.py +0 -0
  91. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_pvt_class.py +0 -0
  92. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_rate.py +0 -0
  93. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_separator.py +0 -0
  94. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_tables.py +0 -0
  95. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/oil/_utils.py +0 -0
  96. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/plyasunov/__init__.py +0 -0
  97. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
  98. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
  99. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  100. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/recommend/__init__.py +0 -0
  101. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/recommend/recommend.py +0 -0
  102. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/sensitivity/__init__.py +0 -0
  103. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  104. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/shared_fns/__init__.py +0 -0
  105. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  106. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/__init__.py +0 -0
  107. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/_aquifer.py +0 -0
  108. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/_decks.py +0 -0
  109. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
  110. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/_relperm.py +0 -0
  111. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/_vfp.py +0 -0
  112. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/simtools/simtools.py +0 -0
  113. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/validate/__init__.py +0 -0
  114. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/pyrestoolbox/validate/validate.py +0 -0
  115. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/setup.cfg +0 -0
  116. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/setup.py +0 -0
  117. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/bessel.rs +0 -0
  118. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/brine_visc/mod.rs +0 -0
  119. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/brine_visc/salt.rs +0 -0
  120. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/brine_visc/water.rs +0 -0
  121. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/critical_properties/mod.rs +0 -0
  122. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/dca/hyperbolic.rs +0 -0
  123. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/dca/mod.rs +0 -0
  124. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/dca/ransac.rs +0 -0
  125. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/gas_viscosity/mod.rs +0 -0
  126. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/gwr.rs +0 -0
  127. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/lib.rs +0 -0
  128. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/matbal/mod.rs +0 -0
  129. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/matbal/objective.rs +0 -0
  130. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/oil/density.rs +0 -0
  131. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/oil/mod.rs +0 -0
  132. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/pseudopressure.rs +0 -0
  133. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/spycher_pruess/mod.rs +0 -0
  134. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/spycher_pruess/solubility.rs +0 -0
  135. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/alpha.rs +0 -0
  136. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/bip.rs +0 -0
  137. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/components.rs +0 -0
  138. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/eos.rs +0 -0
  139. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/fugacity.rs +0 -0
  140. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/k_init.rs +0 -0
  141. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/mod.rs +0 -0
  142. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vle/rachford_rice.rs +0 -0
  143. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/constants.rs +0 -0
  144. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/friction.rs +0 -0
  145. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/holdup_bb.rs +0 -0
  146. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/holdup_gray.rs +0 -0
  147. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/holdup_wg.rs +0 -0
  148. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/ift.rs +0 -0
  149. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/mod.rs +0 -0
  150. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/pvt_helpers.rs +0 -0
  151. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/vlp/static_column.rs +0 -0
  152. {pyrestoolbox-3.8.0 → pyrestoolbox-3.8.2}/src/zfactor/mod.rs +0 -0
@@ -152,7 +152,7 @@ dependencies = [
152
152
 
153
153
  [[package]]
154
154
  name = "pyrestoolbox-native"
155
- version = "3.8.0"
155
+ version = "3.8.2"
156
156
  dependencies = [
157
157
  "pyo3",
158
158
  "rug",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pyrestoolbox-native"
3
- version = "3.8.0"
3
+ version = "3.8.2"
4
4
  edition = "2021"
5
5
  license = "GPL-3.0-or-later"
6
6
  description = "Rust acceleration extensions for pyResToolbox"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 3.8.0
3
+ Version: 3.8.2
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+)
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrestoolbox"
7
- version = "3.8.0"
7
+ version = "3.8.2"
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"}]
@@ -67,6 +67,14 @@ R_GAS = 8.314462 # J/(mol·K)
67
67
  OMEGA_A = 0.45724
68
68
  OMEGA_B = 0.07780
69
69
  MW_NACL = 58.4428 # NaCl molar weight (matches brine.MWSAL)
70
+ # flash_tp trivial-solution test: max|ln K| below this means both labelled
71
+ # phases collapsed onto one composition (K -> 1). Gas-water K-values sit
72
+ # orders of magnitude from 1, so a genuine split never comes near it.
73
+ TRIVIAL_LNK_TOL = 1e-6
74
+ # Tangent-plane stability (Michelsen 1982, Fluid Phase Equilib. 9:1-19): a
75
+ # non-trivial stationary point with sum(Y) above 1 + this makes the feed
76
+ # unstable. The margin keeps round-off at sum(Y) = 1 from reading as a split.
77
+ STABILITY_TM_TOL = 1e-8
70
78
  MW_H2O = 18.015
71
79
 
72
80
  # H2 critical temperature for BIP correlations (manuscript value, NIST)
@@ -1130,6 +1138,11 @@ def rr_solver(
1130
1138
  return N_it, yi, xi, V, L
1131
1139
 
1132
1140
 
1141
+ def _is_trivial_k(K: np.ndarray) -> bool:
1142
+ """True when every K is within TRIVIAL_LNK_TOL of 1 in log terms."""
1143
+ return bool(np.max(np.abs(np.log(K))) < TRIVIAL_LNK_TOL)
1144
+
1145
+
1133
1146
  def solve_rachford_rice(z: np.ndarray, K: np.ndarray) -> Tuple[float, np.ndarray, np.ndarray]:
1134
1147
  """
1135
1148
  Convenience wrapper around rr_solver for flash calculations.
@@ -2059,6 +2072,16 @@ class SWMultiComponentFlash:
2059
2072
  tol: Convergence tolerance on K-values
2060
2073
 
2061
2074
  Returns: V (vapor fraction), x (liquid), y (vapor), converged (bool)
2075
+
2076
+ When successive substitution collapses onto the trivial solution
2077
+ (all K -> 1; e.g. a feed so dilute in gas that the trial vapor stays
2078
+ water-dominated and both phases take the liquid root), a tangent-plane
2079
+ stability test decides. A stable feed returns single phase, converged:
2080
+ V = 0 when water is the majority component (x = z), else V = 1 (y = z).
2081
+ The other composition is the incipient-phase stationary point, or the
2082
+ feed itself when no such phase exists on the cubic. An unstable feed
2083
+ is re-flashed from K seeded by the stability test. converged is False
2084
+ only if the result is still trivial or the test is inconclusive.
2062
2085
  """
2063
2086
  z = np.asarray(z, dtype=float)
2064
2087
  z = z / np.sum(z)
@@ -2107,9 +2130,41 @@ class SWMultiComponentFlash:
2107
2130
  gamma_eff = np.asarray(gamma, dtype=float)
2108
2131
 
2109
2132
  # Initialize K-values (Wilson + gamma for initial estimate)
2110
- K = self._wilson_k_values(T_K, P_Pa) * gamma_eff
2111
- K[self.iw] = min(K[self.iw], 0.01)
2133
+ K0 = self._wilson_k_values(T_K, P_Pa) * gamma_eff
2134
+ K0[self.iw] = min(K0[self.iw], 0.01)
2135
+
2136
+ eos = (Ai, Bi, sqrt_Ai, onemk)
2137
+ K, converged = self._ss_iterate(z, K0, eos, gamma_eff, max_iter, tol)
2138
+
2139
+ # SS collapsed onto K = 1: both labelled phases took one composition,
2140
+ # so V says nothing. Settle it with a stability test instead.
2141
+ if _is_trivial_k(K):
2142
+ verdict = self._resolve_trivial(z, K0, eos, gamma_eff, max_iter, tol)
2143
+ if verdict is not None and verdict[0] == 'stable':
2144
+ _, feed_liquid, incipient = verdict
2145
+ if feed_liquid:
2146
+ return 0.0, z.copy(), incipient, True
2147
+ return 1.0, incipient, z.copy(), True
2148
+ if verdict is not None:
2149
+ K, converged = self._ss_iterate(z, verdict[1], eos, gamma_eff,
2150
+ max_iter, tol)
2151
+
2152
+ # Final compositions with converged K
2153
+ V, x, y = solve_rachford_rice(z, K)
2154
+ x = np.clip(x, 1e-15, None)
2155
+ y = np.clip(y, 1e-15, None)
2156
+ x = x / np.sum(x)
2157
+ y = y / np.sum(y)
2158
+ if _is_trivial_k(K):
2159
+ converged = False
2160
+ return V, x, y, converged
2112
2161
 
2162
+ def _ss_iterate(self, z: np.ndarray, K: np.ndarray, eos: tuple,
2163
+ gamma_eff: np.ndarray, max_iter: int,
2164
+ tol: float) -> Tuple[np.ndarray, bool]:
2165
+ """Damped successive substitution from K. Returns (K, converged)."""
2166
+ Ai, Bi, sqrt_Ai, onemk = eos
2167
+ K = K.copy()
2113
2168
  converged = False
2114
2169
  damp = 0.5
2115
2170
  prev_err = np.inf
@@ -2145,15 +2200,84 @@ class SWMultiComponentFlash:
2145
2200
  prev_err = err
2146
2201
 
2147
2202
  K = K * (K_new / K)**damp
2203
+ return K, converged
2148
2204
 
2149
- # Final compositions with converged K
2150
- V, x, y = solve_rachford_rice(z, K)
2151
- x = np.clip(x, 1e-15, None)
2152
- y = np.clip(y, 1e-15, None)
2153
- x = x / np.sum(x)
2154
- y = y / np.sum(y)
2155
- return V, x, y, converged
2205
+ def _stability_trial(self, lnf_feed: np.ndarray, seed: np.ndarray,
2206
+ trial_phase: str, eos: tuple, ln_gamma: np.ndarray,
2207
+ max_iter: int, tol: float) -> Optional[Tuple[float, np.ndarray]]:
2208
+ """
2209
+ Tangent-plane stationary point by direct substitution (Michelsen 1982):
2210
+ ln Y_i = ln f_i(z) - ln phi_i(y), less ln gamma_i for a liquid trial.
2156
2211
 
2212
+ Returns (sum Y, normalised y), or None when the cubic fails or the
2213
+ search does not settle (inconclusive).
2214
+ """
2215
+ Ai, Bi, sqrt_Ai, onemk = eos
2216
+ lnY = np.log(np.clip(seed / np.sum(seed), 1e-300, None))
2217
+ for _ in range(max_iter):
2218
+ Y = np.exp(lnY)
2219
+ phi = self._calc_fugacity_fast(Y / np.sum(Y), Ai, Bi, sqrt_Ai, onemk,
2220
+ trial_phase)
2221
+ if not np.all(np.isfinite(phi)):
2222
+ return None
2223
+ lnY_new = lnf_feed - np.log(phi)
2224
+ if trial_phase == 'liquid':
2225
+ lnY_new = lnY_new - ln_gamma
2226
+ step = np.max(np.abs(lnY_new - lnY))
2227
+ lnY = lnY_new
2228
+ if step < tol:
2229
+ Y = np.exp(lnY)
2230
+ return float(np.sum(Y)), Y / np.sum(Y)
2231
+ return None
2232
+
2233
+ def _resolve_trivial(self, z: np.ndarray, K0: np.ndarray, eos: tuple,
2234
+ gamma_eff: np.ndarray, max_iter: int, tol: float):
2235
+ """
2236
+ Stability test on the feed after SS collapsed onto K = 1.
2237
+
2238
+ The feed is taken as aqueous (liquid root, with gamma) when water is
2239
+ the majority component, else as the non-aqueous phase (vapor root).
2240
+ Vapor-like trials seeded from K0*z and from the gas components, and
2241
+ liquid-like trials seeded from z/K0 and from pure water, search for a
2242
+ second phase; any unstable trial wins.
2243
+
2244
+ Returns ('stable', feed_liquid, incipient) where incipient is the
2245
+ opposite-phase stationary point (the feed itself when that trial
2246
+ collapsed, i.e. no such phase exists on the cubic), ('split', K) with
2247
+ K seeded from an unstable trial, or None when inconclusive.
2248
+ """
2249
+ Ai, Bi, sqrt_Ai, onemk = eos
2250
+ feed_liquid = bool(z[self.iw] > 0.5)
2251
+ ln_gamma = np.log(gamma_eff)
2252
+ zc = np.clip(z, 1e-300, None)
2253
+ phi_z = self._calc_fugacity_fast(zc, Ai, Bi, sqrt_Ai, onemk,
2254
+ 'liquid' if feed_liquid else 'vapor')
2255
+ if not np.all(np.isfinite(phi_z)):
2256
+ return None
2257
+ lnf = np.log(zc) + np.log(phi_z) + (ln_gamma if feed_liquid else 0.0)
2258
+
2259
+ # Wilson-type seeds alone can start water-dominated and collapse onto
2260
+ # the feed even when a split exists, so each trial phase also gets a
2261
+ # near-pure seed: gas-only for the vapor, water-only for the liquid.
2262
+ not_w = np.arange(len(z)) != self.iw
2263
+ gas_seed = np.where(not_w, zc, 0.0) + 1e-10
2264
+ water_seed = np.where(not_w, 1e-10, 1.0)
2265
+ incipient = z.copy()
2266
+ seeds = (('vapor', K0 * zc), ('vapor', gas_seed),
2267
+ ('liquid', zc / K0), ('liquid', water_seed))
2268
+ for phase, seed in seeds:
2269
+ res = self._stability_trial(lnf, seed, phase, eos, ln_gamma,
2270
+ max_iter, tol)
2271
+ if res is None:
2272
+ return None
2273
+ sum_Y, y = res
2274
+ if _is_trivial_k(y / zc):
2275
+ continue
2276
+ if sum_Y > 1.0 + STABILITY_TM_TOL:
2277
+ return 'split', (y / zc if phase == 'vapor' else zc / y)
2278
+ if (phase == 'vapor') == feed_liquid:
2279
+ incipient = y
2280
+ return 'stable', feed_liquid, incipient
2157
2281
 
2158
2282
  def calc_equilibrium(self, T_K: float, P_Pa: float, z: np.ndarray,
2159
2283
  salinity_method: str = 'gamma_phi',
@@ -1,3 +1,11 @@
1
+ Changelist in 3.8.2:
2
+
3
+ - **BRINE (internal VLE flash): feeds far from saturation now return a proper single-phase result instead of an arbitrary phase label (GitHub issue #5).** The Soreide-Whitson flash behind ``calc_gas_brine_equilibrium`` (``SWMultiComponentFlash.flash_tp``) used successive substitution with no stability test. When the trial phase started on the feed's side of the cubic, both phases took the same root, the K-values collapsed to 1 and Rachford-Rice reported V = 0 or V = 1 from a rounding-level residual, with ``converged=True``; the Python and Rust paths could disagree on which. This hit about a third of a broad sweep of feeds (very water-rich or very gas-rich). The reported case, 1 kg water with 5e-7 mol each of H2 and CH4 at 1 mol/kg NaCl, 45 degC and 100 bar, came back all vapour. A collapsed iteration is now settled by a Michelsen (1982) tangent-plane stability test with Wilson and near-pure trial phases, in Python and Rust (parity 1.5e-14). A stable feed returns single phase and converged, as aqueous (V = 0, x = feed) when water is the majority component and as the non-aqueous phase (V = 1) otherwise; the other composition is the incipient phase where one exists. An unstable feed is re-flashed from the stability test's K-values. ``converged=False`` is returned only if that still fails. Converged two-phase results are unchanged, so ``SoreideWhitson`` and every documented value are unaffected: the public path flashes gas-excess feeds only. ``SWMultiComponentFlash`` remains internal; there is no public interface for splitting a finite inventory into phase amounts.
4
+
5
+ Changelist in 3.8.1:
6
+
7
+ - **NODAL: condensate that drops out in the tubing is treated as a saturated light liquid, not a dead oil.** The gas VLP marches gave dropped-out condensate the Beggs-Robinson dead-oil viscosity (Rs = 0), which is 2-5 times too viscous for a liquid in equilibrium with gas (up to about 16 cP at low temperature) and made the Woldesemayat-Ghajar BHP with dropout exceed the no-dropout value. The dropped liquid now takes the Beggs-Robinson live-oil viscosity at the Standing (1947) solution GOR at local pressure and temperature, with the bubble point at the local pressure (0.25-1.6 cP for 45-55 API condensate), in Python and Rust. Only gas wells with ``pr`` supplied (condensate dropout on) are affected; for 5 MMscf/d with CGR 10, 10 bbl/d water and 3000 psia reservoir pressure (6000 ft tubing), BHP moves from 877.6 to 850.4 psia for WG and from 635.6 to 652.9 psia for HB (its holdup correlation runs through the liquid viscosity number), with Gray and BB within 0.4%. With dropout on, WG BHP is now below the no-dropout value, as it should be with less liquid in the tubing.
8
+
1
9
  Changelist in 3.8.0:
2
10
 
3
11
  - **NODAL: water viscosity in the VLP marches comes from the library's brine model (documented values move).** The marches used a private water-viscosity formula that evaluated a natural-log fit in degC as a base-10 fit in degF, so water viscosity was 12-30x too low (0.054 cP at 100 degF against 0.68; floored at 0.01 cP above about 200 degF), understating liquid friction and holdup in every method. Both the Python and Rust marches now use the same gas-free brine viscosity ``brine_props`` reports (IAPWS-2008 water with IF97 density, Jones-Dole NaCl ratio, Kestin pressure factor), with the salinity taken from ``wsg`` via McCain's gamma_w = 1 + 0.695e-6 ppm; Rust carries a port of that chain matching Python to 1e-14. Wells without water are unchanged. With water the change is modest for small water rates (the documented gas examples with 10 bbl/d move by -0.1 to -45.6 psi; WG moves most because realistic viscosity puts the liquid Reynolds number below Chisholm's laminar threshold) and larger with more water (5 MMscf/d with 500 bbl/d: HB +9%, WG +5%). The pure-Python march is slower (about 15 ms per ``fbhp`` against 3); the Rust path is about 0.2-0.3 ms.
@@ -1048,8 +1048,17 @@ def _condensate_dropout(cgr, qg_mmscfd, p_avg, pr, osg, qw_bwpd, wsg):
1048
1048
 
1049
1049
 
1050
1050
  def _condensate_vis(pr, cgr_local, gsg, api, temp_f, p_avg, oil_vis):
1051
+ """Viscosity (cP) of condensate dropped out in the tubing.
1052
+
1053
+ The dropped liquid is in equilibrium with the gas at the local pressure, so
1054
+ it is a saturated live liquid: Beggs-Robinson live-oil viscosity at the
1055
+ Standing (1947) solution GOR at local p and T, with Pb = p. Before 3.8.1 it
1056
+ was given the dead-oil viscosity (Rs = 0), 2-5x too viscous for a light
1057
+ condensate (up to ~16 cP at low temperature).
1058
+ """
1051
1059
  if pr > 14.7 and cgr_local > 0.01:
1052
- return _oil_viscosity_full(gsg, api, temp_f, rsb=0.0, pb=14.7,
1060
+ rs_sat = _standing_rs(gsg, p_avg, temp_f, api)
1061
+ return _oil_viscosity_full(gsg, api, temp_f, rsb=rs_sat, pb=p_avg,
1053
1062
  press_psia=p_avg)
1054
1063
  return oil_vis
1055
1064
 
@@ -12,6 +12,17 @@ use crate::vle::fugacity::calc_fugacity_fast;
12
12
  use crate::vle::k_init::sw_kvalue_init;
13
13
  use crate::vle::rachford_rice::solve_rachford_rice;
14
14
 
15
+ /// Trivial-solution test for `flash_tp`: max|ln K| below this means both
16
+ /// labelled phases collapsed onto one composition (K -> 1). Gas-water K-values
17
+ /// sit orders of magnitude from 1, so a genuine split never comes near it.
18
+ /// Same value as Python `_lib_vle_engine.TRIVIAL_LNK_TOL`.
19
+ const TRIVIAL_LNK_TOL: f64 = 1e-6;
20
+
21
+ /// Tangent-plane stability (Michelsen 1982, Fluid Phase Equilib. 9:1-19): a
22
+ /// non-trivial stationary point with sum(Y) above 1 + this makes the feed
23
+ /// unstable. Same value as Python `_lib_vle_engine.STABILITY_TM_TOL`.
24
+ const STABILITY_TM_TOL: f64 = 1e-8;
25
+
15
26
  /// Precomputed EOS quantities that are constant across SS iterations (T,P fixed).
16
27
  struct EosPrecomputed {
17
28
  ai_dim: Vec<f64>, // Dimensionless Ai = ai*P/(RT)^2
@@ -110,7 +121,14 @@ fn precompute_eos(
110
121
  /// * `tol` - Convergence tolerance on K-values
111
122
  ///
112
123
  /// # Returns
113
- /// (V, x, y, converged) - vapor fraction, liquid comp, vapor comp, convergence flag
124
+ /// (V, x, y, converged) - vapor fraction, liquid comp, vapor comp, convergence flag.
125
+ /// When successive substitution collapses onto the trivial solution (all
126
+ /// K -> 1), a tangent-plane stability test decides: a stable feed returns
127
+ /// single phase, converged (V = 0 with x = z when water is the majority
128
+ /// component, else V = 1 with y = z; the other composition is the incipient
129
+ /// stationary point, or the feed when none exists); an unstable feed is
130
+ /// re-flashed from the trial's K. `converged` is false only if the result is
131
+ /// still trivial or the test is inconclusive. Port of Python `flash_tp`.
114
132
  pub fn flash_tp(
115
133
  t_k: f64,
116
134
  p_pa: f64,
@@ -149,22 +167,68 @@ pub fn flash_tp(
149
167
 
150
168
  // Initialize K-values (SW-specific + gamma for initial estimate)
151
169
  let k_init = sw_kvalue_init(comp_indices, &tc, &pc, &omega, t_k, p_pa);
152
- let mut k_vals: Vec<f64> = k_init
170
+ let mut k0: Vec<f64> = k_init
153
171
  .iter()
154
172
  .zip(gamma.iter())
155
173
  .map(|(&ki, &gi)| ki * gi)
156
174
  .collect();
157
175
 
158
176
  // Water K should be << 1 in gas-water systems
159
- k_vals[iw] = k_vals[iw].min(0.01);
177
+ k0[iw] = k0[iw].min(0.01);
178
+
179
+ let (mut k_vals, mut converged) = ss_iterate(&z_norm, &k0, &eos, gamma, nc, max_iter, tol);
180
+
181
+ // SS collapsed onto K = 1: both labelled phases took one composition, so
182
+ // V says nothing. Settle it with a stability test instead.
183
+ if is_trivial_k(&k_vals) {
184
+ match resolve_trivial(&z_norm, &k0, &eos, gamma, iw, max_iter, tol) {
185
+ Some(Verdict::Stable { feed_liquid, incipient }) => {
186
+ return if feed_liquid {
187
+ (0.0, z_norm, incipient, true)
188
+ } else {
189
+ (1.0, incipient, z_norm, true)
190
+ };
191
+ }
192
+ Some(Verdict::Split(k_seed)) => {
193
+ (k_vals, converged) = ss_iterate(&z_norm, &k_seed, &eos, gamma, nc, max_iter, tol);
194
+ }
195
+ None => {}
196
+ }
197
+ }
198
+
199
+ // Final compositions with converged K
200
+ let (v, mut x, mut y) = solve_rachford_rice(&z_norm, &k_vals);
201
+ clip_and_normalize(&mut x);
202
+ clip_and_normalize(&mut y);
160
203
 
204
+ let converged = converged && !is_trivial_k(&k_vals);
205
+
206
+ (v, x, y, converged)
207
+ }
208
+
209
+ /// True when every K is within `TRIVIAL_LNK_TOL` of 1 in log terms.
210
+ fn is_trivial_k(k: &[f64]) -> bool {
211
+ k.iter().map(|k| k.ln().abs()).fold(0.0_f64, f64::max) < TRIVIAL_LNK_TOL
212
+ }
213
+
214
+ /// Damped successive substitution from `k_start`. Returns (K, converged).
215
+ fn ss_iterate(
216
+ z: &[f64],
217
+ k_start: &[f64],
218
+ eos: &EosPrecomputed,
219
+ gamma: &[f64],
220
+ nc: usize,
221
+ max_iter: usize,
222
+ tol: f64,
223
+ ) -> (Vec<f64>, bool) {
224
+ let mut k_vals = k_start.to_vec();
161
225
  let mut converged = false;
162
226
  let mut damp: f64 = 0.5;
163
227
  let mut prev_err: f64 = f64::INFINITY;
164
228
 
165
229
  for _it in 0..max_iter {
166
230
  // Robust RR solver (Nielsen & Lia 2022)
167
- let (_v, mut x, mut y) = solve_rachford_rice(&z_norm, &k_vals);
231
+ let (_v, mut x, mut y) = solve_rachford_rice(z, &k_vals);
168
232
 
169
233
  // Clip and normalize
170
234
  clip_and_normalize(&mut x);
@@ -206,13 +270,118 @@ pub fn flash_tp(
206
270
  k_vals[i] *= (k_new[i] / k_vals[i]).powf(damp);
207
271
  }
208
272
  }
273
+ (k_vals, converged)
274
+ }
209
275
 
210
- // Final compositions with converged K
211
- let (v, mut x, mut y) = solve_rachford_rice(&z_norm, &k_vals);
212
- clip_and_normalize(&mut x);
213
- clip_and_normalize(&mut y);
276
+ /// Outcome of the stability test after a trivial SS exit.
277
+ enum Verdict {
278
+ /// Feed is one phase; `incipient` is the opposite-phase stationary point
279
+ /// (the feed itself when that trial collapsed).
280
+ Stable { feed_liquid: bool, incipient: Vec<f64> },
281
+ /// Feed splits; K seeded from the unstable trial.
282
+ Split(Vec<f64>),
283
+ }
214
284
 
215
- (v, x, y, converged)
285
+ /// Tangent-plane stationary point by direct substitution (Michelsen 1982):
286
+ /// ln Y_i = ln f_i(z) - ln phi_i(y), less ln gamma_i for a liquid trial.
287
+ /// Returns (sum Y, normalised y), or None when phi is non-finite or the
288
+ /// search does not settle. Port of Python `_stability_trial`.
289
+ #[allow(clippy::too_many_arguments)]
290
+ fn stability_trial(
291
+ lnf_feed: &[f64],
292
+ seed: &[f64],
293
+ liquid: bool,
294
+ eos: &EosPrecomputed,
295
+ ln_gamma: &[f64],
296
+ nc: usize,
297
+ max_iter: usize,
298
+ tol: f64,
299
+ ) -> Option<(f64, Vec<f64>)> {
300
+ let seed_sum: f64 = seed.iter().sum();
301
+ let mut ln_y: Vec<f64> = seed.iter().map(|&s| (s / seed_sum).max(1e-300).ln()).collect();
302
+ for _ in 0..max_iter {
303
+ let big_y: Vec<f64> = ln_y.iter().map(|v| v.exp()).collect();
304
+ let y_sum: f64 = big_y.iter().sum();
305
+ let y: Vec<f64> = big_y.iter().map(|v| v / y_sum).collect();
306
+ let phi = calc_fugacity_fast(&y, &eos.ai_dim, &eos.bi_dim, &eos.sqrt_ai, &eos.onemk, nc, liquid);
307
+ if !phi.iter().all(|p| p.is_finite()) {
308
+ return None;
309
+ }
310
+ let mut step = 0.0_f64;
311
+ for i in 0..nc {
312
+ let mut v = lnf_feed[i] - phi[i].ln();
313
+ if liquid {
314
+ v -= ln_gamma[i];
315
+ }
316
+ step = step.max((v - ln_y[i]).abs());
317
+ ln_y[i] = v;
318
+ }
319
+ if step < tol {
320
+ let big_y: Vec<f64> = ln_y.iter().map(|v| v.exp()).collect();
321
+ let y_sum: f64 = big_y.iter().sum();
322
+ return Some((y_sum, big_y.iter().map(|v| v / y_sum).collect()));
323
+ }
324
+ }
325
+ None
326
+ }
327
+
328
+ /// Stability test on the feed after SS collapsed onto K = 1. The feed is
329
+ /// taken as aqueous (liquid root, with gamma) when water is the majority
330
+ /// component, else as the non-aqueous phase (vapor root). Vapor-like trials
331
+ /// seeded from K0*z and from the gas components, and liquid-like trials seeded
332
+ /// from z/K0 and from pure water, search for a second phase; any unstable
333
+ /// trial wins. Port of Python `_resolve_trivial`.
334
+ fn resolve_trivial(
335
+ z: &[f64],
336
+ k0: &[f64],
337
+ eos: &EosPrecomputed,
338
+ gamma: &[f64],
339
+ iw: usize,
340
+ max_iter: usize,
341
+ tol: f64,
342
+ ) -> Option<Verdict> {
343
+ let nc = z.len();
344
+ let feed_liquid = z[iw] > 0.5;
345
+ let ln_gamma: Vec<f64> = gamma.iter().map(|g| g.ln()).collect();
346
+ let zc: Vec<f64> = z.iter().map(|&v| v.max(1e-300)).collect();
347
+ let phi_z = calc_fugacity_fast(&zc, &eos.ai_dim, &eos.bi_dim, &eos.sqrt_ai, &eos.onemk, nc, feed_liquid);
348
+ if !phi_z.iter().all(|p| p.is_finite()) {
349
+ return None;
350
+ }
351
+ let lnf: Vec<f64> = (0..nc)
352
+ .map(|i| zc[i].ln() + phi_z[i].ln() + if feed_liquid { ln_gamma[i] } else { 0.0 })
353
+ .collect();
354
+
355
+ // Wilson-type seeds alone can start water-dominated and collapse onto the
356
+ // feed even when a split exists, so each trial phase also gets a near-pure
357
+ // seed: gas-only for the vapor, water-only for the liquid.
358
+ let gas_seed: Vec<f64> = (0..nc).map(|i| if i == iw { 0.0 } else { zc[i] } + 1e-10).collect();
359
+ let water_seed: Vec<f64> = (0..nc).map(|i| if i == iw { 1.0 } else { 1e-10 }).collect();
360
+ let wilson_vap: Vec<f64> = (0..nc).map(|i| k0[i] * zc[i]).collect();
361
+ let wilson_liq: Vec<f64> = (0..nc).map(|i| zc[i] / k0[i]).collect();
362
+ let seeds: [(bool, &[f64]); 4] = [
363
+ (false, &wilson_vap),
364
+ (false, &gas_seed),
365
+ (true, &wilson_liq),
366
+ (true, &water_seed),
367
+ ];
368
+
369
+ let mut incipient = z.to_vec();
370
+ for (liquid, seed) in seeds {
371
+ let (sum_y, y) = stability_trial(&lnf, seed, liquid, eos, &ln_gamma, nc, max_iter, tol)?;
372
+ let k_trial: Vec<f64> = (0..nc).map(|i| y[i] / zc[i]).collect();
373
+ if is_trivial_k(&k_trial) {
374
+ continue;
375
+ }
376
+ if sum_y > 1.0 + STABILITY_TM_TOL {
377
+ let k_seed = if liquid { k_trial.iter().map(|k| 1.0 / k).collect() } else { k_trial };
378
+ return Some(Verdict::Split(k_seed));
379
+ }
380
+ if liquid != feed_liquid {
381
+ incipient = y;
382
+ }
383
+ }
384
+ Some(Verdict::Stable { feed_liquid, incipient })
216
385
  }
217
386
 
218
387
  /// Clip compositions to [1e-15, inf) and normalize.
@@ -86,8 +86,11 @@ fn condensate_vis(
86
86
  pr: f64, cgr_local: f64, gsg: f64, api: f64, temp_f: f64,
87
87
  p_avg: f64, oil_vis: f64,
88
88
  ) -> f64 {
89
+ // Dropped condensate is a saturated live liquid: Beggs-Robinson at the
90
+ // Standing solution GOR at local p, T with Pb = p (mirrors Python _condensate_vis)
89
91
  if pr > 14.7 && cgr_local > 0.01 {
90
- oil_viscosity_full(gsg, api, temp_f, 0.0, 14.7, p_avg, 1.0, 1.0)
92
+ let rs_sat = standing_rs(gsg, p_avg, temp_f, api);
93
+ oil_viscosity_full(gsg, api, temp_f, rs_sat, p_avg, p_avg, 1.0, 1.0)
91
94
  } else {
92
95
  oil_vis
93
96
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes