pyrestoolbox 3.7.1__tar.gz → 3.7.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 (141) hide show
  1. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/PKG-INFO +1 -1
  2. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyproject.toml +1 -1
  3. pyrestoolbox-3.7.2/pyrestoolbox/brine/__init__.py +5 -0
  4. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/brine/brine.py +105 -33
  5. pyrestoolbox-3.7.2/pyrestoolbox/brine/pr_vphi.py +246 -0
  6. pyrestoolbox-3.7.2/pyrestoolbox/brine/vphi_route.py +81 -0
  7. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/brine.rst +4 -4
  8. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/changelist.rst +18 -0
  9. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/plyasunov/iapws_if97.py +36 -1
  10. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/plyasunov/plyasunov_model.py +11 -0
  11. pyrestoolbox-3.7.1/pyrestoolbox/brine/__init__.py +0 -1
  12. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/.github/workflows/build-wheels.yml +0 -0
  13. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/.github/workflows/tests.yml +0 -0
  14. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/.gitignore +0 -0
  15. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/Cargo.lock +0 -0
  16. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/Cargo.toml +0 -0
  17. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/LICENSE +0 -0
  18. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/MANIFEST.in +0 -0
  19. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/README.rst +0 -0
  20. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/ResToolbox/privacy_policy.md +0 -0
  21. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/benchmark_rust_vs_python.py +0 -0
  22. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/build_pure_python.py +0 -0
  23. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/DEPLOYMENT.md +0 -0
  24. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/Dockerfile +0 -0
  25. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/README.md +0 -0
  26. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/pyproject.toml +0 -0
  27. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/pyrestoolbox_mcp/__init__.py +0 -0
  28. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/pyrestoolbox_mcp/serialize.py +0 -0
  29. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/pyrestoolbox_mcp/server.py +0 -0
  30. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/mcp/tests/test_server.py +0 -0
  31. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/__init__.py +0 -0
  32. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/_accelerator.py +0 -0
  33. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  34. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/brine/_lib_vle_engine.py +0 -0
  35. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/classes/__init__.py +0 -0
  36. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/classes/classes.py +0 -0
  37. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/constants/__init__.py +0 -0
  38. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/constants/constants.py +0 -0
  39. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/dca/__init__.py +0 -0
  40. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/dca/dca.py +0 -0
  41. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/dca.rst +0 -0
  42. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/examples.ipynb +0 -0
  43. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/gas.rst +0 -0
  44. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/bot.png +0 -0
  45. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  46. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/bot_img.png +0 -0
  47. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  48. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  49. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/influence.png +0 -0
  50. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/properties_df.png +0 -0
  51. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/sgof.png +0 -0
  52. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/img/swof.png +0 -0
  53. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/index.rst +0 -0
  54. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/layer.rst +0 -0
  55. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/library.rst +0 -0
  56. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/matbal.rst +0 -0
  57. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/nodal.rst +0 -0
  58. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
  59. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  60. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/oil.rst +0 -0
  61. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/plyasunov.rst +0 -0
  62. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/recommend.rst +0 -0
  63. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/sensitivity.rst +0 -0
  64. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/docs/simtools.rst +0 -0
  65. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/gas/__init__.py +0 -0
  66. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/gas/_hydrate.py +0 -0
  67. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/gas/gas.py +0 -0
  68. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/layer/__init__.py +0 -0
  69. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/layer/layer.py +0 -0
  70. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/library/__init__.py +0 -0
  71. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/library/component_library.xlsx +0 -0
  72. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/library/library.py +0 -0
  73. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/matbal/__init__.py +0 -0
  74. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/matbal/matbal.py +0 -0
  75. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/nodal/__init__.py +0 -0
  76. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/nodal/nodal.py +0 -0
  77. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/__init__.py +0 -0
  78. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_compressibility.py +0 -0
  79. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_constants.py +0 -0
  80. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_correlations.py +0 -0
  81. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_density.py +0 -0
  82. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_harmonize.py +0 -0
  83. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_pvt_class.py +0 -0
  84. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_rate.py +0 -0
  85. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_separator.py +0 -0
  86. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_tables.py +0 -0
  87. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/oil/_utils.py +0 -0
  88. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/plyasunov/__init__.py +0 -0
  89. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  90. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/recommend/__init__.py +0 -0
  91. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/recommend/recommend.py +0 -0
  92. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/sensitivity/__init__.py +0 -0
  93. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  94. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/shared_fns/__init__.py +0 -0
  95. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  96. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/__init__.py +0 -0
  97. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/_aquifer.py +0 -0
  98. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/_decks.py +0 -0
  99. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
  100. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/_relperm.py +0 -0
  101. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/_vfp.py +0 -0
  102. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/simtools/simtools.py +0 -0
  103. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/validate/__init__.py +0 -0
  104. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/pyrestoolbox/validate/validate.py +0 -0
  105. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/setup.cfg +0 -0
  106. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/setup.py +0 -0
  107. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/bessel.rs +0 -0
  108. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/critical_properties/mod.rs +0 -0
  109. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/dca/hyperbolic.rs +0 -0
  110. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/dca/mod.rs +0 -0
  111. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/dca/ransac.rs +0 -0
  112. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/gas_viscosity/mod.rs +0 -0
  113. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/gwr.rs +0 -0
  114. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/lib.rs +0 -0
  115. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/matbal/mod.rs +0 -0
  116. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/matbal/objective.rs +0 -0
  117. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/oil/density.rs +0 -0
  118. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/oil/mod.rs +0 -0
  119. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/pseudopressure.rs +0 -0
  120. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/spycher_pruess/mod.rs +0 -0
  121. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/spycher_pruess/solubility.rs +0 -0
  122. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/alpha.rs +0 -0
  123. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/bip.rs +0 -0
  124. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/components.rs +0 -0
  125. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/eos.rs +0 -0
  126. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/flash.rs +0 -0
  127. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/fugacity.rs +0 -0
  128. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/k_init.rs +0 -0
  129. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/mod.rs +0 -0
  130. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vle/rachford_rice.rs +0 -0
  131. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/constants.rs +0 -0
  132. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/friction.rs +0 -0
  133. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/holdup_bb.rs +0 -0
  134. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/holdup_gray.rs +0 -0
  135. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/holdup_wg.rs +0 -0
  136. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/ift.rs +0 -0
  137. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/march.rs +0 -0
  138. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/mod.rs +0 -0
  139. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/pvt_helpers.rs +0 -0
  140. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/vlp/static_column.rs +0 -0
  141. {pyrestoolbox-3.7.1 → pyrestoolbox-3.7.2}/src/zfactor/mod.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 3.7.1
3
+ Version: 3.7.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.7.1"
7
+ version = "3.7.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"}]
@@ -0,0 +1,5 @@
1
+ from .brine import *
2
+ from . import pr_vphi
3
+ from .pr_vphi import V2_inf as V_phi_pr
4
+ from . import vphi_route
5
+ from .vphi_route import V_phi, route_used
@@ -442,7 +442,7 @@ BBL2CUFT = 5.614583333 # cuft in a bbl
442
442
  #CO2 Corrected Brine Density Molar volume of dissolved CO2 estimated with Garcia (2001) equation, used with xCO2 calculated
443
443
  # from Spycher & Pruess, and CO2-free brine density from Spivey et al to calculate insitu density
444
444
  #Pure Brine viscosity Mao-Duan (2009) approach for pure brine viscosity
445
- #CO2 Corrected Brine Viscosity Used approach from "Viscosity Models and Effects of Dissolved CO2", Islam-Carlson (2012)
445
+ #CO2 Corrected Brine Viscosity Calabrese et al. (2019) Eq. 25 T-dependent increment (supersedes Islam-Carlson 2012)
446
446
  # to adjust the pure brine viscosity for xCO2 calculated from Spycher & Pruess
447
447
  #===================================================================================================================
448
448
 
@@ -1123,7 +1123,7 @@ class CO2_Brine_Mixture():
1123
1123
  2. Brine Salt Correction: Spivey et al. (modified)
1124
1124
  3. Pure Brine viscosity: Mao-Duan (2009)
1125
1125
  4. CO2 Corrected Brine Density: Garcia (2001)
1126
- 5. CO2 Corrected Brine Viscosity: Islam-Carlson (2012)
1126
+ 5. CO2 Corrected Brine Viscosity: Calabrese et al. (2019) Eq. 25
1127
1127
 
1128
1128
  pBar: Pressure (Bar)
1129
1129
  degc: Temperature (deg C)
@@ -1229,20 +1229,29 @@ class CO2_Brine_Mixture():
1229
1229
 
1230
1230
  # -- Correcting brine density for dissolved CO2, JE Garcia, LBNL Report# 49023, Oct 2011, "Density of Aqueous Solutions of CO2"
1231
1231
  def garciaDensity(rhoBRnoCO2, tKel, pBar, ppm, xCO2, MwB, MwG):
1232
- # Algebraic reformulation of Garcia Eq 18: original form
1233
- # (1 + mRat*xRat) / (vPhi*xRat/MwB + 1/rhoBRnoCO2) with xRat = xCO2/(1-xCO2)
1234
- # has a singularity at xCO2 -> 1. Multiplying numerator and denominator by
1235
- # (1-xCO2)*MwB removes xRat entirely and gives the same result.
1236
- xNotCO2 = 1.0 - xCO2 #--Brine (H20+Salt) Mole Fraction
1237
- mRat = MwG / MwB #--Mole Weight Ratio , Gas/Brine
1232
+ # Garcia Eq 18 written per kg of water, which removes both the
1233
+ # xCO2 -> 1 singularity of the original form and the mole-basis
1234
+ # mismatch it used to carry: xCO2 here is Spycher-Pruess on the
1235
+ # fully-ionised basis (H2O + Na+ + Cl-) while MwB is the paired-NaCl
1236
+ # brine mole weight, so xNotCO2*MwB overstated the solvent mass by
1237
+ # 8% at 5 molal and understated the CO2 densification to match.
1238
+ #
1239
+ # V_phi is defined against the whole gas-free brine, so:
1240
+ # rho = (Wb + mCO2*MwG) / (Wb/rhoBRnoCO2 + mCO2*vPhi)
1241
+ # with Wb the gas-free brine mass per kg of water and mCO2 the CO2
1242
+ # molality recovered from the ionised-basis mole fraction.
1238
1243
  vPhi = partMolVol(tKel) #--Apparent Molar Volume of Dissolved CO2
1239
- return MwB * (xNotCO2 + mRat * xCO2) / (vPhi * xCO2 + MwB * xNotCO2 / rhoBRnoCO2)
1244
+ mCO2 = xCO2 * (CONMOLA + 2 * m) / (1.0 - xCO2) #--gram mol CO2 / kg water
1245
+ Wb = 1000.0 + m * MWSAL #--gas-free brine mass per kg water, g
1246
+ return (Wb + mCO2 * MwG) / (Wb / rhoBRnoCO2 + mCO2 * vPhi)
1240
1247
 
1241
1248
  # Correct CO2 free brine viscosity for dissolved CO2
1242
- # Using approach from "Viscosity Models and Effects of Dissolved CO2", Akand W. Islam and Eric S. Carlson (Jul 2012), Energy Fuels 2012, 26, 8, 5330�5336, https://doi.org/10.1021/ef3006228
1249
+ # Calabrese, McBride-Wright, Maitland & Trusler (2019), JCED 64, 3831-3847, Eq. 25
1250
+ # (274-449 K, to 100 MPa, salt-type and molality independent, m <= 6).
1251
+ # Supersedes Islam-Carlson (2012), which overstates the slope 3.5x at 200 degF.
1243
1252
  def co2_vis_brine(cP_brine, xCO2):
1244
1253
  # Uses CO2 free brine viscosity (cP) and mole fraction CO2 in brine (xCO2), and returns cP
1245
- return cP_brine * (1 + 4.65 * xCO2**1.0134)
1254
+ return cP_brine * np.exp(_CAL_E1 * np.exp(-_CAL_E2 * (tKel / _CAL_T0 - 1.0)) * xCO2)
1246
1255
 
1247
1256
  # Density and viscosity at specified pressure & temperature (No CO2)
1248
1257
  sg_brine, rhowtp = brine_denw(Mpa) # rhowtp is density of pure water
@@ -1326,16 +1335,51 @@ def make_pvtw_table(*args, **kwargs):
1326
1335
  # ============================================================================
1327
1336
  # Viscosity correction constants for dissolved gases
1328
1337
  # ============================================================================
1329
- _IC_A_CO2 = 4.65 # Islam-Carlson (2012) CO2 coefficient
1330
- _IC_A_H2S = 1.50 # Calibrated from Murphy & Gaines (1974)
1338
+ # H2S: Murphy & Gaines (1974) Table IV, DOI 10.1021/je60063a015, re-based
1339
+ # 2026-07-25 onto the solubility source THAT PAPER USED, Burgess & Germann
1340
+ # (1969) AIChE J 15:272. Murphy & Gaines report viscosity ratios but no mole
1341
+ # fractions; taking x_H2S from Soreide-Whitson instead put it 4.5 to 9.2% high
1342
+ # against Burgess & Germann, and since a scales as 1/x it biased the coefficient
1343
+ # low by the same amount. Implied coefficients on the correct basis are 2.31,
1344
+ # 1.54, 1.98, 1.31 and 0.27; the adopted value is the mean of the four below
1345
+ # 35 degC. Was 1.64 (S&W basis), and 1.50 before the primary source was read.
1346
+ _IC_A_H2S = 1.79 # Murphy & Gaines Table IV on Burgess & Germann solubility
1331
1347
  _IC_B = 1.0134 # Islam-Carlson exponent
1332
1348
 
1349
+ # Calabrese et al. (2019) Eq. 25 CO2 viscosity increment (JCED 64:3831):
1350
+ # ln(mu/mu_brine) = e1 * exp(-e2*(T_K/T0 - 1)) * x_CO2
1351
+ # 274-449 K, to 100 MPa, salt-type and molality independent (m <= 6).
1352
+ # Supersedes the T-independent Islam-Carlson form, which overstates the
1353
+ # slope 3.5x at 200 degF (adopted 2026-07-19).
1354
+ _CAL_E1 = 65.560
1355
+ _CAL_E2 = 2.468
1356
+ _CAL_T0 = 142.0 # K
1357
+
1333
1358
  # Ostermann (1985) SPE 14211 CH4 plateau coefficients
1334
1359
  # mu_sat/mu_free = c0 + c1*T + c2*T^2 (T in degF)
1335
1360
  # NOTE: SPE 14211 prints c2 as 1.0933e-5 (typo); correct is 1.0933e-6
1336
- _OST_C0 = 1.109
1337
- _OST_C1 = -5.98e-4
1338
- _OST_C2 = 1.0933e-6
1361
+ # CH4 dissolved-gas viscosity: unified Arrhenius x Langmuir, fitted to the FULL
1362
+ # Ostermann SPE 14211 dataset (Tables 1a-1c, 23 measurements at 100/150/250 degF
1363
+ # and 500-7000 psi):
1364
+ #
1365
+ # mu_sat/mu_free = 1 + _CH4_A * exp(_CH4_B / T_K) * x / (_CH4_K + x)
1366
+ #
1367
+ # One equation, three parameters, no cap. Arrhenius in T (activated flow; the
1368
+ # hydration structure carrying the excess is destroyed thermally, B = 1240 K =
1369
+ # 10.3 kJ/mol, about half a water hydrogen bond) and Langmuir in x (finite
1370
+ # structurable capacity). Linear in x as x -> 0 as the dilute limit requires,
1371
+ # and saturating naturally so no min() is needed.
1372
+ #
1373
+ # Supersedes a four-parameter construction (linear-in-x ramp spliced to a
1374
+ # temperature-only plateau by min()) built from the paper's three SUMMARY
1375
+ # plateau values. Reading the primary tables overturned its two premises: the
1376
+ # measured plateau is not flat (it drifts up 1.0-1.6 pp from 2000 to 7000 psi),
1377
+ # and Rsw is tabulated so x need not come from an EOS. Fit quality on the 23
1378
+ # points: this form 0.723 pp RMS with 3 parameters, the previous capped ramp
1379
+ # 0.713 pp with 4, an unsaturated linear form 1.146 pp with 2.
1380
+ _CH4_A = 1.71739196e-03
1381
+ _CH4_B = 1239.77535 # K
1382
+ _CH4_K = 1.52860547e-03 # half-saturation mole fraction
1339
1383
 
1340
1384
  # HC component molecular weights for SG-based splitting
1341
1385
  _MW_C1 = 16.043
@@ -1368,7 +1412,10 @@ _SW_GAS_MW = {
1368
1412
  }
1369
1413
 
1370
1414
  # Plyasunov model (internal submodule)
1371
- from pyrestoolbox.plyasunov import V_phi as _plyasunov_V_phi, gas_mw as _plyasunov_gas_mw
1415
+ from pyrestoolbox.plyasunov import gas_mw as _plyasunov_gas_mw
1416
+ from pyrestoolbox.brine.vphi_route import (V_phi as _V_phi,
1417
+ DEFAULT_ROUTE as _VPHI_ROUTE,
1418
+ ROUTES as _VPHI_ROUTES)
1372
1419
 
1373
1420
  # VLE engine (local copy in brine package)
1374
1421
  from pyrestoolbox.brine._lib_vle_engine import calc_gas_brine_equilibrium as _calc_gas_brine_equilibrium
@@ -1386,8 +1433,11 @@ class SoreideWhitson:
1386
1433
  - Mole fraction of vaporised water in gas phase
1387
1434
  - Water content of gas (stb/mmscf, lb/mmscf)
1388
1435
  - Gas solubility in water (sm3/sm3 or scf/stb) — total and per-gas
1389
- - Gas-saturated brine density via Garcia (2001) Eq. 18 with Plyasunov V_phi
1390
- - Gas-saturated brine viscosity with per-gas corrections (Islam-Carlson, Ostermann, Murphy-Gaines)
1436
+ - Gas-saturated brine density via Garcia (2001) Eq. 18. V_phi comes from
1437
+ the S&W modified-PR route by default (vphi_route='auto'), falling back
1438
+ to Plyasunov where PR is not calibrated; 'plyasunov' forces the old
1439
+ default and reproduces results from before 2026-07-25.
1440
+ - Gas-saturated brine viscosity with per-gas corrections (Calabrese CO2, Ostermann CH4, Murphy-Gaines H2S)
1391
1441
  - Brine FVF, compressibility, viscosibility
1392
1442
 
1393
1443
  Supports fresh and saline water (NaCl equivalent).
@@ -1466,7 +1516,17 @@ class SoreideWhitson:
1466
1516
  def __init__(self, pres=None, temp=None, ppm=None, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0,
1467
1517
  sg=0.65, metric=False, cw_sat=False,
1468
1518
  framework='proposed', salinity_method='gamma_phi',
1519
+ vphi_route=_VPHI_ROUTE,
1469
1520
  *, p=None, degf=None, wt=None):
1521
+ # V_phi source for the Garcia density step. 'auto' (default) is the S&W
1522
+ # modified-PR route with one volume shift per gas, falling back to
1523
+ # Plyasunov where PR is not calibrated; 'plyasunov' forces the pre
1524
+ # 2026-07-25 default. See brine/vphi_route.py.
1525
+ if vphi_route not in _VPHI_ROUTES:
1526
+ raise ValueError(
1527
+ f"vphi_route must be one of {_VPHI_ROUTES}, got {vphi_route!r}")
1528
+ self.vphi_route = vphi_route
1529
+
1470
1530
  # Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
1471
1531
  if pres is None and p is not None:
1472
1532
  pres = p
@@ -1797,7 +1857,11 @@ class SoreideWhitson:
1797
1857
  rho_sc_brine_gcc = rho_sc_fw_gcc * salt_ratio_sc
1798
1858
 
1799
1859
  # ================================================================
1800
- # Step 3: Density correction via Garcia Eq. 18 + Plyasunov V_phi
1860
+ # Step 3: Density correction via Garcia Eq. 18 + V_phi.
1861
+ # V_phi comes from the S&W modified-PR route with one volume shift per
1862
+ # gas, falling back to Plyasunov outside its calibration box or for
1863
+ # C3H8/NC4H10. See brine/vphi_route.py for why and for the size of the
1864
+ # step at the boundary.
1801
1865
  # ================================================================
1802
1866
  if self.x_total > 0:
1803
1867
  # Compute mole-fraction-weighted effective V_phi and MW
@@ -1808,17 +1872,25 @@ class SoreideWhitson:
1808
1872
  continue
1809
1873
  yi = x_i / self.x_total # Fraction among dissolved gases
1810
1874
  gas_ply = _VLE_TO_PLYASUNOV.get(gas_vle, gas_vle.upper())
1811
- vphi_eff += yi * _plyasunov_V_phi(gas_ply, tKel, Mpa)
1875
+ vphi_eff += yi * _V_phi(gas_ply, tKel, Mpa, self.vphi_route)
1812
1876
  mw_eff += yi * _plyasunov_gas_mw(gas_ply)
1813
1877
 
1814
1878
  # Garcia Eq. 18 in g/cm3, algebraically reformulated to remove the
1815
1879
  # x1 -> 0 singularity. Multiplying top/bot of
1816
1880
  # (1 + x2*M2/(M1*x1)) / (x2*V_phi/(M1*x1) + 1/rho1)
1817
1881
  # by M1*x1 gives the equivalent, non-singular form below.
1818
- x1 = 1.0 - self.x_total
1819
- M1 = MWWAT
1820
- numerator = M1 * x1 + self.x_total * mw_eff
1821
- denominator = self.x_total * vphi_eff + M1 * x1 / rho_brine_gcc
1882
+ #
1883
+ # SOLVENT BASIS: V_phi is defined against the whole gas-free brine
1884
+ # (V_phi = (V_solution - V_brine)/n_gas), so x2*V_phi is added to
1885
+ # the brine volume, not to the water-only part of it. The flash x
1886
+ # is salt-free, so (1-x2) moles of water carry a brine mass of
1887
+ # W = (1-x2)*MWWAT/(1-S), S being the NaCl weight fraction. Using
1888
+ # water mass alone inflates the dissolved-gas density effect by
1889
+ # 1/(1-S): +5.6% at 1 mol/kg, +29% at 5 mol/kg NaCl.
1890
+ S_wt = wt / 100.0
1891
+ W = (1.0 - self.x_total) * MWWAT / (1.0 - S_wt)
1892
+ numerator = W + self.x_total * mw_eff
1893
+ denominator = self.x_total * vphi_eff + W / rho_brine_gcc
1822
1894
  rho_gas_brine_gcc = numerator / denominator
1823
1895
  else:
1824
1896
  rho_gas_brine_gcc = rho_brine_gcc
@@ -1834,12 +1906,13 @@ class SoreideWhitson:
1834
1906
  continue
1835
1907
  gas_upper = gas_vle.upper()
1836
1908
  if gas_upper == 'CO2':
1837
- vis_factor *= (1.0 + _IC_A_CO2 * x_i ** _IC_B)
1909
+ T_K = (degf - 32.0) / 1.8 + 273.15
1910
+ vis_factor *= np.exp(_CAL_E1 * np.exp(-_CAL_E2 * (T_K / _CAL_T0 - 1.0)) * x_i)
1838
1911
  elif gas_upper == 'H2S':
1839
1912
  vis_factor *= (1.0 + _IC_A_H2S * x_i ** _IC_B)
1840
1913
  elif gas_upper == 'CH4':
1841
- ratio = _OST_C0 + _OST_C1 * degf + _OST_C2 * degf ** 2
1842
- vis_factor *= max(ratio, 1.0)
1914
+ T_K = (degf - 32.0) / 1.8 + 273.15
1915
+ vis_factor *= 1.0 + _CH4_A * np.exp(_CH4_B / T_K) * x_i / (_CH4_K + x_i)
1843
1916
  # C2H6, N2, H2, C3H8, nC4H10: no correction (factor *= 1.0)
1844
1917
 
1845
1918
  vis_gas_brine = vis_base_cP * vis_factor
@@ -1915,12 +1988,11 @@ class SoreideWhitson:
1915
1988
  continue
1916
1989
  yi = x_i / self.x_total
1917
1990
  gas_ply = _VLE_TO_PLYASUNOV.get(gas_vle, gas_vle.upper())
1918
- vphi_eff_p1 += yi * _plyasunov_V_phi(gas_ply, tKel, Mpa_p1)
1991
+ vphi_eff_p1 += yi * _V_phi(gas_ply, tKel, Mpa_p1, self.vphi_route)
1919
1992
 
1920
- # Garcia Eq. 18 at P+1, same algebraically reformulated (non-singular)
1921
- # form as Step 3 — multiply top/bot through by M1*x1.
1922
- numerator_p1 = MWWAT * x1 + self.x_total * mw_eff
1923
- denom_p1 = self.x_total * vphi_eff_p1 + MWWAT * x1 / rho_brine_p1_gcc
1993
+ # Garcia Eq. 18 at P+1, same gas-free-brine solvent basis as Step 3.
1994
+ numerator_p1 = W + self.x_total * mw_eff
1995
+ denom_p1 = self.x_total * vphi_eff_p1 + W / rho_brine_p1_gcc
1924
1996
  rho_p1_gcc = numerator_p1 / denom_p1
1925
1997
  else:
1926
1998
  rho_p1_gcc = rho_brine_p1_gcc
@@ -0,0 +1,246 @@
1
+ """V2inf of dissolved gases from the Soreide-Whitson modified PR EOS + VSHIFT.
2
+
3
+ An alternative to `plyasunov_model.V2_inf`, built 2026-07-25. It is NOT a
4
+ replacement: Plyasunov remains the reference and covers 8 gases to 573 K. This
5
+ route exists because it is far more parsimonious and because it repairs the one
6
+ V_phi defect the project has pinned (the H2S temperature trend).
7
+
8
+ THE EXACT RELATION. For any pressure-explicit EOS the partial molar volume is
9
+
10
+ Vbar_2 = -(dP/dn2)_{T,V,n1} / (dP/dV)_{T,n}
11
+
12
+ and at x2 -> 0 that is V2inf. No flash and no root selection for the solute: the
13
+ volume root is the water-rich LIQUID root, and the gas enters only through b2
14
+ and the cross term a12 = sqrt(a1 a2)(1 - kij). The solute never has a root of
15
+ its own, so there is nothing to "force" into a liquid state.
16
+
17
+ WHAT TO SAY WHEN CHALLENGED ON PR'S POOR WATER PROPERTIES. Two separate points,
18
+ and the first is a scope point rather than a numerical one:
19
+
20
+ 1. PR's water volumetrics never reach the delivered answer. This module
21
+ supplies V_phi ONLY. The solvent density rho1 in the Garcia mixing rule
22
+ comes from Spivey (`brine_properties.rho_brine`), never from this EOS. So
23
+ PR's water error is confined to an intermediate quantity that is then
24
+ calibrated against measured V_phi.
25
+
26
+ 2. A constant shift absorbs an OFFSET but cannot fix a SHAPE, so the question
27
+ that matters is whether V_phi's T and P dependence survives. The answer is
28
+ empirical: one constant s per gas reproduces Hnedkovsky densimetry to 0.5%
29
+ mean absolute error for H2S, 0.9% for CO2 and 1.4% for CH4 across
30
+ 298-473 K. Lead with that measurement, not with a story about errors
31
+ cancelling.
32
+
33
+ Do NOT justify the temperature cap by citing PR's 15-17% water density error -
34
+ that error is a FLAT systematic factor (density ratio PR/IF97 is 0.847 at 298 K
35
+ and 0.820 at 473 K) and is precisely what the shift absorbs. The cap exists for
36
+ two other reasons, given at `T_MAX`.
37
+
38
+ VOLUME SHIFT. Peneloux translation v = v_EOS - sum(n_i c_i) gives, exactly,
39
+
40
+ V2inf(shifted) = V2inf(EOS) - c2, c2 = s2 * b2
41
+
42
+ so a constant s2 is a pure offset with no temperature or pressure shape. The
43
+ shift is stored dimensionless (s = c/b, the standard VSHIFT convention) and
44
+ b2 is the gas's PR co-volume.
45
+
46
+ ONE PARAMETER PER GAS, NOT TWO. A held-out test (`fit_pr_vshift.py`) showed
47
+ s(T) = s0 + s1/T overfits: on the 21 Murphy & Gaines H2S points it scored 1.3%
48
+ mean absolute error against 0.6% for a constant s. Constant s is used.
49
+
50
+ CALIBRATION BASIS: DIRECT VOLUMETRIC MEASUREMENT ONLY. See `fit_pr_vshift.py`
51
+ for the set and the exclusions. Indirect determinations (solubility-versus-
52
+ pressure, electrochemical) and V_phi inverted from brine density are excluded
53
+ from the fit, the latter because they scatter five to seven times wider.
54
+
55
+ PROVENANCE. Developed in ~/projects/garcia_extension (`code/pr_vphi_model.py`,
56
+ calibrated by `code/fit_pr_vshift.py`) and ported here 2026-07-25. The S&W alpha,
57
+ the aqueous kij correlations and the critical properties are taken from this
58
+ package's own `_lib_vle_engine`, not re-copied.
59
+ """
60
+ import numpy as np
61
+
62
+ from . import _lib_vle_engine as _SW
63
+ from ..plyasunov.iapws_if97 import p_sat_if97
64
+
65
+ R = 8.31446261815324 # cm3.MPa/(mol.K), so volumes come out in cm3/mol
66
+
67
+ # PR constants (Peng-Robinson 1976)
68
+ _OMEGA_A = 0.45723553
69
+ _OMEGA_B = 0.07779607
70
+
71
+ # Validity envelope. Two reasons for the 473.15 K ceiling, neither of them the
72
+ # 15-17% water density error (that is a flat offset and the shift absorbs it):
73
+ # (a) the calibration data stops there. Hnedkovsky's top point is 473.15 K, so
74
+ # anything above is unvalidated extrapolation of a fitted shift.
75
+ # (b) V2inf diverges at the SOLVENT critical point, and a cubic EOS gets the
76
+ # near-critical behaviour wrong in a way no constant offset can repair.
77
+ # Measured on this implementation, the PR/IF97 compressibility ratio for
78
+ # water is 0.27 at 298 K, 0.84 at 473 K, crosses 1.0 near 520 K and reaches
79
+ # 1.32 by 613 K - i.e. a growing SHAPE error, not an offset. It shows up
80
+ # downstream as H2S drifting 9% by 523 K and 36% by 573 K.
81
+ T_MIN, T_MAX = 273.15, 623.15
82
+ T_CALIBRATED_MAX = 473.15 # top of the calibration data (Hnedkovsky)
83
+ T_VOUCHED_MAX = 450.0 # ~350 degF; the only one of the three that is an
84
+ # accuracy claim. T_MAX is where the arithmetic
85
+ # stops (IF97 Region 1); T_CALIBRATED_MAX is where
86
+ # the fitted shift stops being fitted.
87
+ P_MAX = 100.0
88
+
89
+ SUPPORTED = ('CH4', 'CO2', 'H2S', 'N2', 'H2', 'C2H6')
90
+
91
+ # WATER ALPHA INSENSITIVITY, measured 2026-07-25 and worth knowing before
92
+ # porting. The S&W framework uses `alpha_water_soreide` for the sw_original and
93
+ # dropin frameworks but Mathias-Copeman (`alpha_water_mc3`) otherwise, and the
94
+ # ResToolbox3 Dart path uses MC3. Swapping the water alpha moves V2inf_raw by
95
+ # only 0.00 to 0.05 cm3/mol over 298-473 K, i.e. delta-s of 0.0002 to 0.0018,
96
+ # under 2% of the shift values and negligible against the 0.5-1.4% fit error.
97
+ # So ONE VSHIFT set is valid for both water alphas and the ports do not need
98
+ # separate calibrations. (The alpha enters only a1, and its effect largely
99
+ # cancels in the ratio of derivatives.)
100
+
101
+ # Dimensionless volume shifts s = c2/b2, fitted to direct volumetric
102
+ # measurements only by `fit_pr_vshift.py`. Regenerate with that script if any
103
+ # calibration point changes; the values below are pinned in validation.py.
104
+ VSHIFT = {
105
+ 'CH4': -0.109632,
106
+ 'CO2': -0.037913,
107
+ 'H2S': -0.078975,
108
+ 'N2': -0.155288,
109
+ 'H2': -0.177625,
110
+ 'C2H6': -0.073142,
111
+ }
112
+
113
+ # Gases whose shift rests on 298 K data alone, so their temperature behaviour is
114
+ # the EOS's unaided prediction and is NOT calibrated. Reported, not hidden.
115
+ UNCALIBRATED_IN_T = ('N2', 'H2', 'C2H6')
116
+
117
+
118
+ def _ab(species, T, m_nacl=0.0):
119
+ """PR a(T) and b for one species, cm6.MPa/mol2 and cm3/mol."""
120
+ c = _SW.COMPONENTS[species]
121
+ Pc = c.Pc / 1e6
122
+ Tr = T / c.Tc
123
+ if species == 'H2O':
124
+ alpha = _SW.alpha_water_soreide(Tr, m_nacl)
125
+ else:
126
+ alpha = _SW.alpha_standard_pr(Tr, c.omega)
127
+ return (_OMEGA_A * R ** 2 * c.Tc ** 2 / Pc * alpha,
128
+ _OMEGA_B * R * c.Tc / Pc)
129
+
130
+
131
+ def b_covolume(gas):
132
+ """PR co-volume of the gas, cm3/mol - the normaliser for the VSHIFT."""
133
+ return _ab(gas, 300.0)[1]
134
+
135
+
136
+ def _P_mix(T, V, n1, n2, gas, kij, m_nacl):
137
+ """PR pressure of the binary water + gas mixture, MPa. V in cm3, n in mol."""
138
+ a1, b1 = _ab('H2O', T, m_nacl)
139
+ a2, b2 = _ab(gas, T)
140
+ n = n1 + n2
141
+ x1, x2 = n1 / n, n2 / n
142
+ a12 = np.sqrt(a1 * a2) * (1.0 - kij)
143
+ am = x1 * x1 * a1 + 2.0 * x1 * x2 * a12 + x2 * x2 * a2
144
+ bm = x1 * b1 + x2 * b2
145
+ return (n * R * T / (V - n * bm)
146
+ - n * n * am / (V * V + 2.0 * n * bm * V - n * n * bm * bm))
147
+
148
+
149
+ def v_water_liquid(T, P, m_nacl=0.0):
150
+ """Smallest real PR root for water/brine, cm3/mol.
151
+
152
+ Above water's critical temperature the cubic has one real root; below it the
153
+ smallest root is the liquid. Taking the minimum root is continuous across
154
+ both cases, which is what the derivative below needs.
155
+ """
156
+ a, b = _ab('H2O', T, m_nacl)
157
+ A = a * P / (R * T) ** 2
158
+ B = b * P / (R * T)
159
+ roots = np.roots([1.0, -(1.0 - B), A - 3.0 * B ** 2 - 2.0 * B,
160
+ -(A * B - B ** 2 - B ** 3)])
161
+ real = roots[np.abs(roots.imag) < 1e-9].real
162
+ real = real[real > B]
163
+ if real.size == 0:
164
+ raise ValueError(f'No admissible PR liquid root for water at {T} K, {P} MPa')
165
+ return float(real.min()) * R * T / P
166
+
167
+
168
+ def _check(gas, T, P):
169
+ if gas not in SUPPORTED:
170
+ raise ValueError(
171
+ f'{gas!r} has no fitted VSHIFT. Supported: {SUPPORTED}. Use '
172
+ f'plyasunov_model.V2_inf for the other gases.')
173
+ if not (T_MIN <= T <= T_MAX):
174
+ raise ValueError(
175
+ f'T = {T} K is outside the calibrated range {T_MIN}-{T_MAX} K. '
176
+ f'Above {T_MAX} K the fitted shift is unvalidated (the calibration '
177
+ f'data stops there) and the near-critical shape error grows; use '
178
+ f'plyasunov_model.V2_inf instead.')
179
+ if not (0.0 < P <= P_MAX):
180
+ raise ValueError(f'P = {P} MPa is outside 0 to {P_MAX} MPa.')
181
+ psat = float(p_sat_if97(T))
182
+ if P < psat:
183
+ raise ValueError(
184
+ f'P = {P} MPa is below the water saturation pressure {psat:.4f} MPa '
185
+ f'at {T} K, so the solvent is vapour and V2inf is undefined.')
186
+
187
+
188
+ def V2_inf_raw(gas, T, P, m_nacl=0.0):
189
+ """Unshifted V2inf from the exact EOS relation, cm3/mol. No range guard."""
190
+ V = v_water_liquid(T, P, m_nacl)
191
+ kij = _SW.get_kij_aq(gas, T, m_nacl)
192
+ # dP/dn2 at fixed T, V, n1: one-sided, because n2 = 0 is the boundary
193
+ h = 1e-7
194
+ dPdn2 = (_P_mix(T, V, 1.0, h, gas, kij, m_nacl)
195
+ - _P_mix(T, V, 1.0, 0.0, gas, kij, m_nacl)) / h
196
+ dV = V * 1e-7
197
+ dPdV = (_P_mix(T, V + dV, 1.0, 0.0, gas, kij, m_nacl)
198
+ - _P_mix(T, V - dV, 1.0, 0.0, gas, kij, m_nacl)) / (2.0 * dV)
199
+ return -dPdn2 / dPdV
200
+
201
+
202
+ def V2_inf(gas, T, P, m_nacl=0.0, s=None):
203
+ """Apparent molar volume of the dissolved gas at infinite dilution, cm3/mol.
204
+
205
+ Args:
206
+ gas: one of SUPPORTED
207
+ T: K, within T_MIN..T_MAX and above the water saturation pressure
208
+ P: MPa, up to P_MAX
209
+ m_nacl: NaCl molality. Enters the S&W water alpha and kij only; there is
210
+ NO salinity term on the volume shift, consistent with the project's
211
+ decision that a salt effect on V_phi is real but not applied
212
+ (see salt_effect_vphi.py). Leave at 0 for the calibrated behaviour.
213
+ s: override the dimensionless volume shift (for refitting).
214
+
215
+ Returns:
216
+ V2inf in cm3/mol.
217
+ """
218
+ _check(gas, T, P)
219
+ shift = VSHIFT[gas] if s is None else s
220
+ return V2_inf_raw(gas, T, P, m_nacl) - shift * b_covolume(gas)
221
+
222
+
223
+ # `plyasunov_model` exposes V_phi as an alias for V2_inf; mirror that so the two
224
+ # modules are drop-in comparable in scripts.
225
+ V_phi = V2_inf
226
+
227
+
228
+ def _demo():
229
+ print('S&W PR + VSHIFT, V2inf in cm3/mol (Plyasunov in brackets)')
230
+ from ..plyasunov import V2_inf as V_ply
231
+ print(f"\n {'gas':<6}{'s':>10}{'b':>8} " +
232
+ ''.join(f'{t:>18.0f} K' for t in (298, 348, 398, 448)))
233
+ for g in SUPPORTED:
234
+ cells = []
235
+ for T in (298.15, 348.15, 398.15, 448.15):
236
+ cells.append(f'{V2_inf(g, T, 20.0):7.2f} '
237
+ f'[{float(V_ply(g, T, 20.0)):6.2f}]')
238
+ flag = ' *' if g in UNCALIBRATED_IN_T else ''
239
+ print(f' {g:<6}{VSHIFT[g]:10.4f}{b_covolume(g):8.2f} '
240
+ + ''.join(f'{c:>20}' for c in cells) + flag)
241
+ print('\n * temperature behaviour is the EOS unaided: these gases have no '
242
+ 'T-resolved\n calibration data, only a 298 K cluster.')
243
+
244
+
245
+ __all__ = ['V2_inf', 'V_phi', 'V2_inf_raw', 'b_covolume', 'v_water_liquid',
246
+ 'VSHIFT', 'SUPPORTED', 'UNCALIBRATED_IN_T', 'T_MIN', 'T_MAX', 'P_MAX']
@@ -0,0 +1,81 @@
1
+ """Which V_phi route supplies the dissolved-gas molar volume.
2
+
3
+ DEFAULT CHANGED 2026-07-25: the delivered gas-saturated brine density uses the
4
+ Soreide-Whitson modified-PR route with one dimensionless volume shift per gas,
5
+ falling back to the Plyasunov A12inf model where PR is not calibrated. Plyasunov
6
+ was the default only because it was implemented first.
7
+
8
+ WHY PR IS PREFERRED. Against direct volumetric measurement PR wins on five of
9
+ six gases (in-sample MAE: CH4 1.4 vs 1.5%, H2S 0.5 vs 1.4%, N2 3.3 vs 4.9%,
10
+ H2 5.1 vs 6.2%, C2H6 3.0 vs 3.4%; it loses CO2, 0.9 vs 0.7%), and it is the only
11
+ route that reproduces the H2S temperature trend - Murphy and Gaines measured
12
+ +0.69 cm3/mol between their cold and warm groups, PR gives +0.53, Plyasunov is
13
+ flat at +0.02. It also costs one number per gas rather than 35 at eight
14
+ significant digits plus IAPWS-IF97.
15
+
16
+ NO TEMPERATURE HANDOVER (2026-07-25). An earlier version fell back to Plyasunov
17
+ above 473 K, the top of the calibration data. It no longer does: PR runs to the
18
+ IAPWS-IF97 Region 1 ceiling of 623.15 K. A mid-range handover bought nothing and
19
+ put a discontinuity in the delivered default.
20
+
21
+ That is not an accuracy claim. Accuracy is vouched for to about 300-350 degF
22
+ (420-450 K); above that neither route is calibrated and neither is claimed. The
23
+ practical envelope therefore sits inside the calibration data, and within it the
24
+ two routes agree on brine density to 0.12% at worst.
25
+
26
+ WHERE PR STILL CANNOT BE USED, and what happens instead:
27
+ * C3H8 and NC4H10 have no fitted volume shift, so they always use Plyasunov.
28
+ * Outside 273.15-623.15 K, above 100 MPa, or below the water saturation
29
+ pressure, where V2inf is undefined because the solvent is vapour.
30
+
31
+ CAVEAT. N2, H2 and C2H6 have shifts fitted to a 298 K cluster only, so for those
32
+ three the temperature dependence of the default route is the EOS unaided rather
33
+ than calibrated. Plyasunov stays available via route='plyasunov'.
34
+ """
35
+
36
+ from pyrestoolbox.plyasunov import V_phi as _plyasunov_V_phi
37
+
38
+ from . import pr_vphi as _pr
39
+
40
+ ROUTES = ("auto", "pr", "plyasunov")
41
+ DEFAULT_ROUTE = "auto"
42
+
43
+
44
+ def pr_applicable(gas: str, T: float, P: float) -> bool:
45
+ """True if the PR + VSHIFT route is calibrated for this gas and state."""
46
+ try:
47
+ _pr._check(gas, T, P)
48
+ return True
49
+ except (ValueError, KeyError):
50
+ return False
51
+
52
+
53
+ def route_used(gas: str, T: float, P: float, route: str = DEFAULT_ROUTE) -> str:
54
+ """Which route V_phi would actually use. For reporting, never for logic."""
55
+ if route in ("pr", "plyasunov"):
56
+ return route
57
+ return "pr" if pr_applicable(gas, T, P) else "plyasunov"
58
+
59
+
60
+ def V_phi(gas: str, T: float, P: float, route: str = DEFAULT_ROUTE,
61
+ m_nacl: float = 0.0) -> float:
62
+ """Dissolved-gas apparent molar volume at infinite dilution, cm3/mol.
63
+
64
+ Args:
65
+ gas: gas name ('CO2', 'CH4', 'H2S', 'N2', 'H2', 'C2H6', 'C3H8', 'NC4H10')
66
+ T: temperature, K
67
+ P: pressure, MPa
68
+ route: 'auto' uses PR where calibrated and Plyasunov elsewhere; 'pr'
69
+ forces PR and raises outside its box; 'plyasunov' forces A12inf.
70
+ m_nacl: NaCl molality, passed to the PR route only, where it enters the
71
+ S&W water alpha and kij. There is no salinity term on the volume
72
+ shift itself; leave at 0 for the calibrated behaviour.
73
+ """
74
+ if route not in ROUTES:
75
+ raise ValueError(f"route must be one of {ROUTES}, got {route!r}")
76
+ if route == "plyasunov":
77
+ return float(_plyasunov_V_phi(gas, T, P))
78
+ if route == "pr":
79
+ return float(_pr.V2_inf(gas, T, P, m_nacl))
80
+ return (float(_pr.V2_inf(gas, T, P, m_nacl)) if pr_applicable(gas, T, P)
81
+ else float(_plyasunov_V_phi(gas, T, P)))
@@ -450,11 +450,11 @@ Pure CO2 case at 5000 psia x 275 deg F and 3% NaCl brine:
450
450
  >>> from pyrestoolbox import brine
451
451
  >>> mix = brine.SoreideWhitson(pres=5000, temp=275, ppm=30000, y_CO2=1.0, metric=False)
452
452
  >>> mix.bDen # Returns [Gas Saturated, Gas-Free Brine, Freshwater]
453
- [0.973376988330695, 0.968164592979362, 0.9476497407774847]
453
+ [0.9731117228519672, 0.968164592979362, 0.9476497407774847]
454
454
  >>> mix.Rs # Returns per-gas Rs dict (scf/stb)
455
- {'CO2': 139.57908352590252}
455
+ {'CO2': 139.5790835259016}
456
456
  >>> mix.bw # Returns [Gas Saturated, Gas-Free, Freshwater]
457
- [1.096443890843725, 1.0543023174291248, 1.0542033190822462]
457
+ [1.096613263262556, 1.0543023174291248, 1.0542033190822462]
458
458
 
459
459
  Pure CH4 case (SG=0.554) at 5000 psia x 275 deg F and 3% NaCl brine:
460
460
 
@@ -464,7 +464,7 @@ Pure CH4 case (SG=0.554) at 5000 psia x 275 deg F and 3% NaCl brine:
464
464
  >>> mix.Rs
465
465
  {'CH4': 21.21234560600256}
466
466
  >>> mix.bDen
467
- [0.9641136914617666, 0.968164592979362, 0.9476497407774847]
467
+ [0.9641391918061833, 0.968164592979362, 0.9476497407774847]
468
468
 
469
469
  Mixed gas (10% CO2, 5% H2S, SG=0.7) at 200 Bar x 80 degC and 10,000 ppm NaCl:
470
470