pyrestoolbox 3.8.2__tar.gz → 3.8.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 (152) hide show
  1. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/workflows/tests.yml +26 -0
  2. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/Cargo.lock +1 -1
  3. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/Cargo.toml +1 -1
  4. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/PKG-INFO +2 -2
  5. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/README.rst +1 -1
  6. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/Dockerfile +1 -1
  7. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyproject.toml +2 -2
  8. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/server.py +24 -14
  9. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/tests/test_server.py +41 -10
  10. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyproject.toml +1 -1
  11. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_salting_library.py +1 -1
  12. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_vle_engine.py +73 -70
  13. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/brine.py +31 -10
  14. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/pr_vphi.py +10 -2
  15. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/vphi_route.py +2 -1
  16. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/classes/classes.py +2 -2
  17. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/constants/constants.py +3 -2
  18. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/brine.rst +59 -13
  19. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/changelist.rst +18 -0
  20. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/examples.ipynb +17 -17
  21. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/gas.rst +184 -39
  22. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/index.rst +1 -1
  23. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/matbal.rst +4 -1
  24. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal.rst +2 -2
  25. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal_examples.ipynb +12 -12
  26. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/oil.rst +2 -2
  27. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/recommend.rst +10 -4
  28. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/_hydrate.py +7 -3
  29. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/gas.py +142 -124
  30. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/matbal/matbal.py +6 -3
  31. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/nodal/nodal.py +6 -7
  32. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/plyasunov_model.py +1 -0
  33. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/recommend/recommend.py +11 -6
  34. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/shared_fns/shared_fns.py +4 -12
  35. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/validate/validate.py +2 -2
  36. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/bessel.rs +5 -0
  37. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/critical_properties/mod.rs +12 -6
  38. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/gas_viscosity/mod.rs +32 -25
  39. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/pseudopressure.rs +14 -14
  40. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/bip.rs +23 -0
  41. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/components.rs +9 -4
  42. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/k_init.rs +13 -1
  43. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/zfactor/mod.rs +67 -53
  44. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/complexity-baseline.json +0 -0
  45. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/scripts/check_complexity.py +0 -0
  46. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/workflows/build-wheels.yml +0 -0
  47. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.gitignore +0 -0
  48. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/LICENSE +0 -0
  49. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/MANIFEST.in +0 -0
  50. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/ResToolbox/privacy_policy.md +0 -0
  51. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/build_pure_python.py +0 -0
  52. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/clippy.toml +0 -0
  53. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/README.md +0 -0
  54. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/__init__.py +0 -0
  55. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/serialize.py +0 -0
  56. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/__init__.py +0 -0
  57. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/_accelerator.py +0 -0
  58. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/__init__.py +0 -0
  59. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_dielectric.py +0 -0
  60. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_maoduan_viscosity.py +0 -0
  61. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_pitzer_params.py +0 -0
  62. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/appelo_volumes.py +0 -0
  63. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/iapws_viscosity.py +0 -0
  64. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/jones_dole_viscosity.py +0 -0
  65. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/kestin_nacl_viscosity.py +0 -0
  66. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/viscosity_route.py +0 -0
  67. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/classes/__init__.py +0 -0
  68. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/constants/__init__.py +0 -0
  69. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/dca/__init__.py +0 -0
  70. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/dca/dca.py +0 -0
  71. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/dca.rst +0 -0
  72. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot.png +0 -0
  73. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  74. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot_img.png +0 -0
  75. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  76. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  77. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/influence.png +0 -0
  78. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/properties_df.png +0 -0
  79. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/sgof.png +0 -0
  80. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/swof.png +0 -0
  81. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/layer.rst +0 -0
  82. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/library.rst +0 -0
  83. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  84. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/plyasunov.rst +0 -0
  85. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/sensitivity.rst +0 -0
  86. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/simtools.rst +0 -0
  87. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/__init__.py +0 -0
  88. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/layer/__init__.py +0 -0
  89. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/layer/layer.py +0 -0
  90. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/__init__.py +0 -0
  91. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/component_library.xlsx +0 -0
  92. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/library.py +0 -0
  93. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/matbal/__init__.py +0 -0
  94. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/nodal/__init__.py +0 -0
  95. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/__init__.py +0 -0
  96. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_compressibility.py +0 -0
  97. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_constants.py +0 -0
  98. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_correlations.py +0 -0
  99. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_density.py +0 -0
  100. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_harmonize.py +0 -0
  101. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_pvt_class.py +0 -0
  102. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_rate.py +0 -0
  103. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_separator.py +0 -0
  104. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_tables.py +0 -0
  105. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_utils.py +0 -0
  106. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/__init__.py +0 -0
  107. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
  108. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  109. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/recommend/__init__.py +0 -0
  110. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/sensitivity/__init__.py +0 -0
  111. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  112. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/shared_fns/__init__.py +0 -0
  113. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/__init__.py +0 -0
  114. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_aquifer.py +0 -0
  115. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_decks.py +0 -0
  116. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
  117. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_relperm.py +0 -0
  118. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_vfp.py +0 -0
  119. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/simtools.py +0 -0
  120. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/validate/__init__.py +0 -0
  121. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/setup.cfg +0 -0
  122. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/setup.py +0 -0
  123. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/mod.rs +0 -0
  124. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/salt.rs +0 -0
  125. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/water.rs +0 -0
  126. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/hyperbolic.rs +0 -0
  127. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/mod.rs +0 -0
  128. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/ransac.rs +0 -0
  129. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/gwr.rs +0 -0
  130. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/lib.rs +0 -0
  131. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/matbal/mod.rs +0 -0
  132. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/matbal/objective.rs +0 -0
  133. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/oil/density.rs +0 -0
  134. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/oil/mod.rs +0 -0
  135. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/spycher_pruess/mod.rs +0 -0
  136. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/spycher_pruess/solubility.rs +0 -0
  137. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/alpha.rs +0 -0
  138. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/eos.rs +0 -0
  139. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/flash.rs +0 -0
  140. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/fugacity.rs +0 -0
  141. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/mod.rs +0 -0
  142. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/rachford_rice.rs +0 -0
  143. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/constants.rs +0 -0
  144. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/friction.rs +0 -0
  145. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_bb.rs +0 -0
  146. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_gray.rs +0 -0
  147. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_wg.rs +0 -0
  148. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/ift.rs +0 -0
  149. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/march.rs +0 -0
  150. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/mod.rs +0 -0
  151. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/pvt_helpers.rs +0 -0
  152. {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/static_column.rs +0 -0
@@ -63,6 +63,32 @@ jobs:
63
63
  - name: Run full test suite (Rust + parity)
64
64
  run: python3 -m pytest pyrestoolbox/tests/ -q
65
65
 
66
+ # ── MCP server (separate distribution in mcp/) on both SDK majors ────
67
+ # The server picks FastMCP (1.x) or MCPServer (2.x) at import; test both so
68
+ # an SDK release that breaks either path fails here, not in a deployment.
69
+ test-mcp:
70
+ name: MCP server (${{ matrix.mcp }})
71
+ runs-on: ubuntu-latest
72
+ strategy:
73
+ fail-fast: false
74
+ matrix:
75
+ mcp: ["mcp>=1.2,<2", "mcp>=2,<3"]
76
+ steps:
77
+ - uses: actions/checkout@v5
78
+
79
+ - uses: actions/setup-python@v6
80
+ with:
81
+ python-version: "3.12"
82
+
83
+ - name: Install dependencies
84
+ run: pip install numpy scipy pandas tabulate openpyxl mpmath ilt-inversion pytest "${{ matrix.mcp }}"
85
+
86
+ # Library from the source tree (pure Python), server from mcp/.
87
+ - name: Run MCP tests
88
+ env:
89
+ PYRESTOOLBOX_NO_RUST: "1"
90
+ run: PYTHONPATH="$PWD:$PWD/mcp" python3 -m pytest mcp/tests/ -q
91
+
66
92
  # ── Complexity ratchet (new code only; existing offenders baselined) ─
67
93
  complexity:
68
94
  name: Complexity ratchet
@@ -152,7 +152,7 @@ dependencies = [
152
152
 
153
153
  [[package]]
154
154
  name = "pyrestoolbox-native"
155
- version = "3.8.2"
155
+ version = "3.8.4"
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.2"
3
+ version = "3.8.4"
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.2
3
+ Version: 3.8.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+)
@@ -45,7 +45,7 @@ Includes functions to perform calculations including;
45
45
  - Nodal analysis operating point determination
46
46
  - Eclipse VFP table generation (VFPPROD and VFPINJ keywords)
47
47
  - PVT Calculations for oil
48
- - PVT calculation for gas, including up to 100% inerts for CO2, H2S, N2 and H2
48
+ - PVT calculation for gas, including up to 100% inerts for CO2, H2S, N2, H2 and He
49
49
  - Gas caloric properties (enthalpy, Cp, Cv, Joule-Thomson coefficient) from the BNS tuned Peng Robinson EOS
50
50
  - Gas hydrate formation prediction with thermodynamic inhibitor calculations
51
51
  - Return critical parameters for typical components
@@ -20,7 +20,7 @@ Includes functions to perform calculations including;
20
20
  - Nodal analysis operating point determination
21
21
  - Eclipse VFP table generation (VFPPROD and VFPINJ keywords)
22
22
  - PVT Calculations for oil
23
- - PVT calculation for gas, including up to 100% inerts for CO2, H2S, N2 and H2
23
+ - PVT calculation for gas, including up to 100% inerts for CO2, H2S, N2, H2 and He
24
24
  - Gas caloric properties (enthalpy, Cp, Cv, Joule-Thomson coefficient) from the BNS tuned Peng Robinson EOS
25
25
  - Gas hydrate formation prediction with thermodynamic inhibitor calculations
26
26
  - Return critical parameters for typical components
@@ -7,7 +7,7 @@ FROM python:3.11-slim
7
7
 
8
8
  # Pin the library version for reproducible corporate deployments; bump
9
9
  # deliberately and rebuild.
10
- ARG PYRESTOOLBOX_VERSION=3.7.0
10
+ ARG PYRESTOOLBOX_VERSION=3.8.2
11
11
 
12
12
  WORKDIR /app
13
13
  COPY . /app/mcp
@@ -11,8 +11,8 @@ authors = [{name = "Mark W. Burgoyne", email = "mark.w.burgoyne@gmail.com"}]
11
11
  keywords = ["mcp", "petroleum", "reservoir", "pyrestoolbox"]
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
- "mcp>=1.2,<2",
15
- "pyrestoolbox>=3.7",
14
+ "mcp>=1.2,<3",
15
+ "pyrestoolbox>=3.8.2",
16
16
  ]
17
17
  readme = "README.md"
18
18
 
@@ -17,7 +17,12 @@ as MCP resources (docs://gas, docs://oil, ...).
17
17
  import importlib
18
18
  import inspect
19
19
 
20
- from mcp.server.fastmcp import FastMCP
20
+ try: # mcp >= 2 renamed FastMCP to MCPServer
21
+ from mcp.server.mcpserver import MCPServer as _MCPServer
22
+ _MCP_V2 = True
23
+ except ImportError: # mcp 1.x
24
+ from mcp.server.fastmcp import FastMCP as _MCPServer
25
+ _MCP_V2 = False
21
26
 
22
27
  from pyrestoolbox_mcp.serialize import to_jsonable
23
28
 
@@ -37,7 +42,7 @@ ALLOWED_MODULES = {
37
42
  "sensitivity": "Parameter sweeps and tornado analysis",
38
43
  }
39
44
 
40
- mcp = FastMCP(
45
+ mcp = _MCPServer(
41
46
  "pyrestoolbox",
42
47
  instructions=(
43
48
  "Petroleum/reservoir engineering calculations from the pyrestoolbox "
@@ -250,7 +255,7 @@ def _construct(dotted, key, spec):
250
255
  @mcp.tool()
251
256
  def recommend_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
252
257
  n2: float = 0, h2: float = 0, api: float | None = None,
253
- deviation: float = 0, well_type: str = "gas") -> dict:
258
+ deviation: float = 0, well_type: str = "gas", he: float = 0) -> dict:
254
259
  """Recommend correlation methods for a fluid/well, with rationale.
255
260
 
256
261
  Composition fractions are molar. api: stock tank oil gravity (include to
@@ -263,7 +268,7 @@ def recommend_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
263
268
  from pyrestoolbox import recommend
264
269
  recs = recommend.recommend_methods(sg=sg, co2=co2, h2s=h2s, n2=n2, h2=h2,
265
270
  api=api, deviation=deviation,
266
- well_type=well_type)
271
+ well_type=well_type, he=he)
267
272
  return to_jsonable(recs)
268
273
 
269
274
 
@@ -291,12 +296,12 @@ def co2_brine_props(pres: float, temp: float, ppm: float = 0,
291
296
  def sw_brine_props(pres: float, temp: float, ppm: float = 0,
292
297
  y_CO2: float = 0, y_H2S: float = 0, y_N2: float = 0,
293
298
  y_H2: float = 0, sg: float = 0.65, cw_sat: bool = False,
294
- metric: bool = False) -> dict:
299
+ metric: bool = False, y_He: float = 0) -> dict:
295
300
  """Multicomponent gas-saturated brine properties (Soreide-Whitson VLE).
296
301
 
297
302
  pres: pressure (psia, or barsa if metric=True). temp: temperature
298
303
  (deg F, or deg C if metric=True). ppm: NaCl-equivalent salinity.
299
- y_*: molar fractions of CO2/H2S/N2/H2 in the equilibrium gas; the
304
+ y_*: molar fractions of CO2/H2S/N2/H2/He in the equilibrium gas; the
300
305
  remainder is hydrocarbon gas of specific gravity sg. Returns aqueous
301
306
  composition, brine density/viscosity triplets, Rs, water content and
302
307
  compressibilities.
@@ -304,7 +309,7 @@ def sw_brine_props(pres: float, temp: float, ppm: float = 0,
304
309
  from pyrestoolbox import brine
305
310
  sw = brine.SoreideWhitson(pres=pres, temp=temp, ppm=ppm, y_CO2=y_CO2,
306
311
  y_H2S=y_H2S, y_N2=y_N2, y_H2=y_H2, sg=sg,
307
- cw_sat=cw_sat, metric=metric)
312
+ cw_sat=cw_sat, metric=metric, y_He=y_He)
308
313
  keys = ("x", "bDen", "bVis", "bw", "Rs", "Rs_total", "water_content",
309
314
  "Cf_usat", "Cf_sat")
310
315
  return {k: to_jsonable(getattr(sw, k, None)) for k in keys}
@@ -382,13 +387,18 @@ def build_parser():
382
387
 
383
388
  def main():
384
389
  args = build_parser().parse_args()
385
- if args.transport == "streamable-http":
386
- mcp.settings.host = args.host
387
- mcp.settings.port = args.port
388
- # Independent request handling; no server-side session affinity needed
389
- # behind a load balancer.
390
- mcp.settings.stateless_http = True
391
- mcp.run(transport=args.transport)
390
+ if args.transport != "streamable-http":
391
+ mcp.run(transport=args.transport)
392
+ return
393
+ # stateless_http: independent request handling; no server-side session
394
+ # affinity needed behind a load balancer.
395
+ http = {"host": args.host, "port": args.port, "stateless_http": True}
396
+ if _MCP_V2:
397
+ mcp.run(transport=args.transport, **http)
398
+ else: # 1.x reads these from settings, not run()
399
+ for key, value in http.items():
400
+ setattr(mcp.settings, key, value)
401
+ mcp.run(transport=args.transport)
392
402
 
393
403
 
394
404
  if __name__ == "__main__":
@@ -4,9 +4,9 @@ Run from the repo root:
4
4
  PYTHONPATH=/home/mark/projects/pyResToolbox:/home/mark/projects/pyResToolbox/mcp \
5
5
  python3 -m pytest mcp/tests/ -q
6
6
 
7
- Most tests exercise the tool functions directly (FastMCP's @tool decorator
7
+ Most tests exercise the tool functions directly (the SDK's @tool decorator
8
8
  returns the undecorated function). test_client_session_* run the full MCP
9
- protocol over an in-process transport.
9
+ protocol over an in-process transport. The suite runs on mcp 1.x and 2.x.
10
10
  """
11
11
 
12
12
  import os
@@ -186,15 +186,19 @@ def test_fit_and_forecast_exponential():
186
186
 
187
187
  # ------------------------------------------------- full MCP protocol session
188
188
 
189
+ def _in_memory_client():
190
+ """In-process client session: mcp 2.x Client takes the server directly;
191
+ 1.x needs the memory-stream helper and the low-level server."""
192
+ if srv._MCP_V2:
193
+ from mcp.client import Client
194
+ return Client(srv.mcp)
195
+ from mcp.shared.memory import create_connected_server_and_client_session
196
+ return create_connected_server_and_client_session(srv.mcp._mcp_server)
197
+
198
+
189
199
  @pytest.mark.anyio
190
200
  async def test_client_session_tools_and_resources():
191
- try:
192
- from mcp.shared.memory import create_connected_server_and_client_session
193
- except ImportError:
194
- pytest.skip('mcp.shared.memory helper not available in this SDK version')
195
-
196
- async with create_connected_server_and_client_session(
197
- srv.mcp._mcp_server) as client:
201
+ async with _in_memory_client() as client:
198
202
  tools = await client.list_tools()
199
203
  names = {t.name for t in tools.tools}
200
204
  assert {'list_functions', 'describe', 'call', 'recommend_methods',
@@ -204,7 +208,8 @@ async def test_client_session_tools_and_resources():
204
208
  result = await client.call_tool(
205
209
  'call', {'name': 'gas.gas_z',
206
210
  'arguments': {'p': 2000, 'sg': 0.75, 'degf': 200}})
207
- assert not result.isError
211
+ # 2.x result fields are snake_case
212
+ assert not (result.is_error if srv._MCP_V2 else result.isError)
208
213
  assert '0.868265' in result.content[0].text
209
214
 
210
215
  resources = await client.list_resources()
@@ -230,3 +235,29 @@ def test_cli_parser_defaults_and_http():
230
235
  '--host', '0.0.0.0', '--port', '9000'])
231
236
  assert (args.transport, args.host, args.port) == ('streamable-http',
232
237
  '0.0.0.0', 9000)
238
+
239
+
240
+ def test_main_passes_http_settings_to_the_sdk(monkeypatch):
241
+ """host, port and stateless_http reach the SDK: as run() keywords on
242
+ mcp 2.x, through mcp.settings on 1.x."""
243
+ calls = []
244
+ monkeypatch.setattr(srv.mcp, 'run', lambda **kw: calls.append(kw))
245
+ monkeypatch.setattr('sys.argv', ['pyrestoolbox-mcp', '--transport', 'streamable-http',
246
+ '--host', '0.0.0.0', '--port', '9000'])
247
+ srv.main()
248
+ want = {'host': '0.0.0.0', 'port': 9000, 'stateless_http': True}
249
+ if srv._MCP_V2:
250
+ assert calls == [dict(transport='streamable-http', **want)]
251
+ else:
252
+ assert calls == [{'transport': 'streamable-http'}]
253
+ assert {k: getattr(srv.mcp.settings, k) for k in want} == want
254
+ calls.clear()
255
+ monkeypatch.setattr('sys.argv', ['pyrestoolbox-mcp'])
256
+ srv.main()
257
+ assert calls == [{'transport': 'stdio'}]
258
+
259
+
260
+ def test_sw_brine_props_helium():
261
+ out = srv.sw_brine_props(pres=3000, temp=150, ppm=50000, y_He=1.0)
262
+ assert out['x']['He'] > 0
263
+ assert out['bDen'][0] < out['bDen'][1]
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrestoolbox"
7
- version = "3.8.2"
7
+ version = "3.8.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"}]
@@ -26,7 +26,7 @@ LN10 = np.log(10.0) # 2.302585...
26
26
  # ====================================================================
27
27
  # Boiling points (K) for S&W gases
28
28
  TB_K = {
29
- 'H2': 20.3, 'N2': 77.4, 'CO2': 194.7, 'H2S': 212.8, # H2S NBP 212.87 K (NIST)
29
+ 'H2': 20.3, 'He': 4.2238, 'N2': 77.4, 'CO2': 194.7, 'H2S': 212.8, # H2S NBP 212.87 K (NIST)
30
30
  'CH4': 111.6, 'C2H6': 184.6, 'C3H8': 231.1,
31
31
  'iC4H10': 261.4, 'nC4H10': 272.7, 'iC5H12': 301.0,
32
32
  'nC5H12': 309.2, 'nC6H14': 341.9, 'nC7H16': 371.6,
@@ -106,6 +106,12 @@ COMPONENTS = {
106
106
  # Hydrogen (this work) - Uses Tc=33.145 K (NIST) consistently for both EOS and BIP correlations
107
107
  'H2': ComponentProperties('Hydrogen', 33.145, 1.2964e6, -0.219, 20.3, 2.016),
108
108
 
109
+ # Helium (He extension, 2026-10-02) - NIST constants (Ortiz-Vega et al. 2019 EOS critical
110
+ # point, as in CoolProp 8.0), used consistently for EOS and BIP correlations (Tr = T/5.1953).
111
+ # NOT the 5-component Z-factor model's helium Tc (a deliberately non-standard 6.35 degR, tuned
112
+ # for LBC dilute viscosity); the correlations below are valid only with these constants.
113
+ 'He': ComponentProperties('Helium', 5.1953, 0.228323e6, -0.38354, 4.2238, 4.002602),
114
+
109
115
  # Acid gases
110
116
  'CO2': ComponentProperties('Carbon Dioxide', 304.2, 7.38e6, 0.2273, 194.7, 44.01),
111
117
  'H2S': ComponentProperties('Hydrogen Sulfide', 373.2, 8.94e6, 0.1081, 212.8, 34.082),
@@ -130,7 +136,7 @@ COMPONENTS = {
130
136
  # List of supported gas species (excludes water)
131
137
  GAS_SPECIES = ['H2', 'CO2', 'N2', 'H2S', 'CH4', 'C2H6', 'C3H8',
132
138
  'iC4H10', 'nC4H10', 'iC5H12', 'nC5H12',
133
- 'nC6H14', 'nC7H16', 'nC8H18', 'nC10H22']
139
+ 'nC6H14', 'nC7H16', 'nC8H18', 'nC10H22', 'He']
134
140
 
135
141
 
136
142
  # =============================================================================
@@ -434,6 +440,16 @@ def kij_aq_h2_dropin(T_K: float, salinity_molal: float = 0.0) -> float:
434
440
  Tr = T_K / 33.145
435
441
  return (-14.9412 + Tr) / (2.2832 + 0.3893 * Tr)
436
442
 
443
+ def kij_aq_he_dropin(T_K: float, salinity_molal: float = 0.0) -> float:
444
+ """He: rational, n=130, MAE=0.0183 (S&W alpha; He extension 2026-10-02).
445
+
446
+ Fitted L1 to fugacity-targeted pointwise kij from Gardiner & Smith 1972, Gerth 1983,
447
+ Abrosimov & Lebedeva 2013 (50-80 C), Pray et al. 1952 (163 C) and Potter & Clynne 1978
448
+ (104, 149 C); data 20-163 C, 1-1000 bar. Not validated above 200 C.
449
+ """
450
+ Tr = T_K / 5.1953
451
+ return (-71.6152 + Tr) / (-0.394962 + 0.505807 * Tr)
452
+
437
453
  def kij_aq_ch4_dropin(T_K: float, salinity_molal: float = 0.0) -> float:
438
454
  """CH4: rational, n=115, MAE=0.0089 (S&W alpha)."""
439
455
  Tr = T_K / 190.60
@@ -466,6 +482,7 @@ def _kij_aq_hc_proposed(gas: str):
466
482
  # Proposed: MC-3 alpha + freshwater-only kij. Salinity via Sechenov/gamma-phi.
467
483
  KIJ_AQ_MC3: Dict[str, Callable] = {
468
484
  'H2': kij_aq_h2_proposed,
485
+ 'He': kij_aq_he_dropin, # Default-framework fit, not refitted on MC-3 alpha
469
486
  'CO2': kij_aq_co2_proposed,
470
487
  'N2': kij_aq_n2_proposed,
471
488
  'H2S': kij_aq_h2s_proposed,
@@ -485,6 +502,7 @@ KIJ_AQ_MC3: Dict[str, Callable] = {
485
502
  # S&W original: S&W alpha + embedded salinity in kij for HC/CO2/N2.
486
503
  KIJ_AQ_SW_ORIGINAL: Dict[str, Callable] = {
487
504
  'H2': kij_aq_h2, # Paper 1 rational (S&W had no H2)
505
+ 'He': kij_aq_he_dropin, # He extension (S&W had no He)
488
506
  'CO2': kij_aq_co2, # S&W Eq 14 (embedded salinity)
489
507
  'N2': kij_aq_n2, # S&W Eq 13 (embedded salinity)
490
508
  'H2S': kij_aq_h2s, # S&W Eq 15 (no salinity)
@@ -504,6 +522,7 @@ KIJ_AQ_SW_ORIGINAL: Dict[str, Callable] = {
504
522
  # Drop-in: S&W alpha + freshwater-only kij refitted for S&W alpha + embedded delta.
505
523
  KIJ_AQ_DEFAULT: Dict[str, Callable] = {
506
524
  'H2': kij_aq_h2_dropin,
525
+ 'He': kij_aq_he_dropin,
507
526
  'CO2': kij_aq_co2_dropin,
508
527
  'N2': kij_aq_n2_dropin,
509
528
  'H2S': kij_aq_h2s_dropin,
@@ -536,6 +555,7 @@ EMBEDDED_SALINITY_PARAMS_MC3 = {
536
555
  'CH4': {'Tc': 190.60, 'a0': 0.1383, 'a1': -0.1394, 'a2': 0.0442},
537
556
  'N2': {'Tc': 126.10, 'a0': 0.2257, 'a1': -0.1548, 'a2': 0.0321},
538
557
  'H2': {'Tc': 33.145, 'a0': 0.3833, 'a1': -0.0660, 'a2': 0.0033},
558
+ 'He': {'Tc': 5.1953, 'a0': 0.359923, 'a1': -0.0095344, 'a2': 7.00398e-05}, # From default (not refitted)
539
559
  'C2H6': {'Tc': 305.40, 'a0': 0.0791, 'a1': -0.1263, 'a2': 0.0677},
540
560
  'C3H8': {'Tc': 369.80, 'a0': 0.0606, 'a1': -0.1165, 'a2': 0.0772},
541
561
  'nC4H10': {'Tc': 425.20, 'a0': 0.0488, 'a1': -0.1072, 'a2': 0.0836},
@@ -551,6 +571,7 @@ EMBEDDED_SALINITY_PARAMS_DEFAULT: Dict[str, Dict] = {
551
571
  'CH4': {'Tc': 190.60, 'a0': 0.1304, 'a1': -0.1295, 'a2': 0.0394},
552
572
  'N2': {'Tc': 126.10, 'a0': 0.2173, 'a1': -0.1468, 'a2': 0.0302},
553
573
  'H2': {'Tc': 33.145, 'a0': 0.3658, 'a1': -0.0625, 'a2': 0.0030},
574
+ 'He': {'Tc': 5.1953, 'a0': 0.359923, 'a1': -0.0095344, 'a2': 7.00398e-05}, # He extension 2026-10-02: Sechenov target S&W Eq 8 (Tb 4.2238 K), 25-100 C
554
575
  'C2H6': {'Tc': 305.40, 'a0': 0.0813, 'a1': -0.1287, 'a2': 0.0646},
555
576
  'C3H8': {'Tc': 369.80, 'a0': 0.0606, 'a1': -0.1165, 'a2': 0.0772}, # From proposed (not refitted)
556
577
  'nC4H10': {'Tc': 425.20, 'a0': 0.0488, 'a1': -0.1072, 'a2': 0.0836}, # From proposed (not refitted)
@@ -668,6 +689,7 @@ def get_kij_aq(gas: str, T_K: float, salinity_molal: float = 0.0,
668
689
  # Constant kij_NA values from S&W 1992 Table 5
669
690
  KIJ_NA: Dict[str, Optional[float]] = {
670
691
  'H2': 0.468, # This work
692
+ 'He': 0.468, # SURROGATE (He extension 2026-10-02): H2 value; no He water-content data found. Unvalidated.
671
693
  'CO2': 0.1896, # S&W 1992 (or 0.18756 from Yan 2011)
672
694
  'N2': 0.4778, # S&W 1992
673
695
  'H2S': 0.1610, # This work (constant; replaces S&W Eq 17)
@@ -1534,78 +1556,44 @@ class SWBinaryVLE:
1534
1556
  """
1535
1557
  Calculate water mole fraction in gas phase using specified kij_NA.
1536
1558
 
1559
+ Binary two-phase equilibrium by successive substitution on the K-values,
1560
+ with the phase compositions fixed each pass by the binary closure
1561
+ sum(K_i x_i) = sum(y_i) = 1:
1562
+ x_w = (K_g - 1) / (K_g - K_w), y_w = K_w x_w. Before 3.8.3 the loop
1563
+ normalised K x instead, which any x satisfies, so y_H2O came out as
1564
+ K_w / (K_w + K_g x_g) with x_g wherever the iteration drifted; low by
1565
+ roughly y_H2O itself (0.44 against 0.79 at 200 degF, 14.7 psia).
1566
+
1537
1567
  Args:
1538
1568
  T_K: Temperature in Kelvin
1539
1569
  P_Pa: Pressure in Pascals
1540
1570
  kij_na: Binary interaction parameter for non-aqueous phase
1541
1571
  max_iter: Maximum iterations
1542
1572
 
1543
- Runs the successive substitution twice: the adaptive-damping pass
1544
- (canonical, preserves released behaviour) and a fixed-damping
1545
- verification pass. Near the dew boundary (high T, P within a few
1546
- multiples of Psat) the normalised iteration has multiple fixed
1547
- points; when the passes disagree by more than 0.5% relative the
1548
- answer is path-dependent and np.nan is returned instead of an
1549
- arbitrary branch. Non-convergence also returns np.nan.
1550
-
1551
1573
  Returns:
1552
- y_H2O: Water mole fraction in gas phase (np.nan if the
1553
- iteration fails to converge or is ambiguous)
1574
+ y_H2O: Water mole fraction in gas phase; np.nan when no two-phase
1575
+ split exists (K_w >= 1 or K_g <= 1, e.g. at or below the
1576
+ water vapour pressure) or the iteration does not converge.
1554
1577
  """
1555
- y1, conv1 = self._water_content_ss(T_K, P_Pa, kij_na, max_iter,
1556
- adaptive=True)
1557
- if not conv1:
1558
- return np.nan
1559
- y2, conv2 = self._water_content_ss(T_K, P_Pa, kij_na, 2 * max_iter,
1560
- adaptive=False, damp=0.4)
1561
- if not conv2 or abs(y2 - y1) > 5e-3 * max(abs(y1), 1e-8):
1562
- return np.nan
1563
- return y1
1578
+ y_w, converged = self._water_content_ss(T_K, P_Pa, kij_na, max_iter)
1579
+ return y_w if converged else np.nan
1564
1580
 
1565
1581
  def _water_content_ss(self, T_K: float, P_Pa: float, kij_na: float,
1566
- max_iter: int, adaptive: bool = True,
1567
- damp: float = 0.3) -> Tuple[float, bool]:
1568
- """Damped successive substitution for water content. Returns (y_H2O, converged)."""
1582
+ max_iter: int) -> Tuple[float, bool]:
1583
+ """Binary successive substitution for water content. Returns (y_H2O, converged)."""
1569
1584
  x = np.array([0.999, 0.001])
1570
1585
  y = np.array([0.02, 0.98])
1571
-
1572
- prev_error = np.inf
1573
- for iteration in range(max_iter):
1574
- x = np.clip(x, 1e-14, 1.0 - 1e-14)
1575
- x = x / np.sum(x)
1576
- y = np.clip(y, 1e-14, 1.0 - 1e-14)
1577
- y = y / np.sum(y)
1578
-
1586
+ for _ in range(max_iter):
1579
1587
  K = self._calc_K_with_kij(T_K, P_Pa, x, y, kij_na)
1580
- if not np.all(np.isfinite(K)):
1588
+ if not np.all(np.isfinite(K)) or K[0] >= 1.0 or K[1] <= 1.0:
1581
1589
  return np.nan, False
1582
-
1583
- y_new = K * x
1584
- y_new = np.clip(y_new, 1e-14, 1.0 - 1e-14)
1585
- y_new = y_new / np.sum(y_new)
1586
-
1587
- error = np.max(np.abs(y_new - y))
1588
- if error < 1e-10:
1590
+ x_w = (K[1] - 1.0) / (K[1] - K[0])
1591
+ y_new = np.array([K[0] * x_w, 1.0 - K[0] * x_w])
1592
+ converged = abs(y_new[0] - y[0]) < 1e-12 * max(y_new[0], 1e-8)
1593
+ x = np.array([x_w, 1.0 - x_w])
1594
+ y = y_new
1595
+ if converged:
1589
1596
  return y[0], True
1590
-
1591
- if adaptive:
1592
- if error < prev_error:
1593
- damp = min(damp * 1.15, 0.8)
1594
- else:
1595
- damp = max(damp * 0.5, 0.1)
1596
- prev_error = error
1597
-
1598
- y = y + damp * (y_new - y)
1599
- y = y / np.sum(y)
1600
-
1601
- x_new = y / (K + 1e-30)
1602
- x_new = np.clip(x_new, 1e-14, 1.0 - 1e-14)
1603
- x_new = x_new / np.sum(x_new)
1604
- if x_new[0] < 0.5:
1605
- x_new = np.array([0.98, 0.02])
1606
- x = x + damp * (x_new - x)
1607
- x = x / np.sum(x)
1608
-
1609
1597
  return y[0], False
1610
1598
 
1611
1599
  # Aliases for backward compatibility
@@ -1754,6 +1742,7 @@ _SW_KVALUE_PARAMS = {
1754
1742
  # Light gases: Cross form [a, b, c, d, e, f]
1755
1743
  # Fitted using proposed Burgoyne & Nielsen 2026 freshwater kij_AQ forms (Feb 2026)
1756
1744
  'H2': [6.4295, 25.5844, -0.5985, -50.0000, -0.0007, -3.2598],
1745
+ 'He': [9.7370, 50.0000, -1.0874, 50.0000, 0.0039, 10.0747], # He extension 2026-10-02; b, d at bounds
1757
1746
  'CO2': [-5.9974, 26.3804, -0.9380, -16.3941, 0.0607, 0.3688],
1758
1747
  'N2': [1.4998, 36.4929, -0.6419, -50.0000, 0.0110, -0.6328],
1759
1748
  'H2S': [-1.0549, 7.7334, -1.3646, -3.4035, 0.0850, 0.8651],
@@ -2053,6 +2042,23 @@ class SWMultiComponentFlash:
2053
2042
 
2054
2043
  return gamma
2055
2044
 
2045
+ def _rust_matches(self, mode: str) -> bool:
2046
+ """True if the Rust flash kernel computes what the Python path would.
2047
+
2048
+ The Rust 'default' kernel always adds the embedded delta_kij in AQ mode,
2049
+ whereas the Python path adds it only for salinity_method='embedded'
2050
+ (build_kij_matrix). Any other default pairing with salt present (e.g.
2051
+ the class defaults, 'default' with 'gamma_phi') must take the Python
2052
+ path: sending it to Rust put dissolved-gas x 30-50% apart between the
2053
+ two paths at 2 m NaCl. 'mc3' matches for every salinity_method.
2054
+ """
2055
+ if self.framework == 'mc3':
2056
+ return True
2057
+ if self.framework != 'default':
2058
+ return False
2059
+ return (self.salinity_method == 'embedded' or self.salinity <= 0
2060
+ or mode != 'AQ')
2061
+
2056
2062
  def flash_tp(self, T_K: float, P_Pa: float, z: np.ndarray,
2057
2063
  mode: str = 'AQ', gamma: Optional[np.ndarray] = None,
2058
2064
  max_iter: int = 200,
@@ -2101,7 +2107,8 @@ class SWMultiComponentFlash:
2101
2107
  #
2102
2108
  # 'sw_original' has no Rust implementation and takes the Python path
2103
2109
  # rather than being silently downgraded.
2104
- if _RUST_AVAILABLE and self.framework in ('mc3', 'default'):
2110
+ # See _rust_matches for which framework/salinity pairings qualify.
2111
+ if _RUST_AVAILABLE and self._rust_matches(mode):
2105
2112
  try:
2106
2113
  gamma_arr = np.asarray(gamma, dtype=float) if gamma is not None \
2107
2114
  else np.ones(self.nc)
@@ -2485,6 +2492,7 @@ def calc_gas_brine_equilibrium(
2485
2492
  method: str = 'henry',
2486
2493
  salinity_method: str = 'gamma_phi',
2487
2494
  framework: str = 'default',
2495
+ y_He: float = 0.0,
2488
2496
  ) -> Tuple[Dict[str, float], Dict[str, float]]:
2489
2497
  """
2490
2498
  Calculate gas-brine equilibrium using Soreide-Whitson framework.
@@ -2516,7 +2524,8 @@ def calc_gas_brine_equilibrium(
2516
2524
  salinity_wt_pct: Brine salinity in weight percent NaCl
2517
2525
  temperature_F: Temperature in degrees Fahrenheit
2518
2526
  pressure_psia: Pressure in psia
2519
- y_*: Mole fractions of each gas in dry gas (will be normalized)
2527
+ y_*: Mole fractions of each gas in dry gas (will be normalized).
2528
+ y_He is last in the signature so positional calls are unaffected.
2520
2529
  method: 'henry' (binary decomposition) or 'flash' (multi-component)
2521
2530
  salinity_method: 'gamma_phi', 'explicit', or 'embedded'
2522
2531
  framework: 'default', 'mc3', or 'sw_original' ('dropin' and
@@ -2539,16 +2548,10 @@ def calc_gas_brine_equilibrium(
2539
2548
  salinity_molal = wt_pct_to_molality(salinity_wt_pct)
2540
2549
 
2541
2550
  # Build gas composition dict
2542
- gas_comp = {}
2543
- if y_CH4 > 0: gas_comp['CH4'] = y_CH4
2544
- if y_C2H6 > 0: gas_comp['C2H6'] = y_C2H6
2545
- if y_C3H8 > 0: gas_comp['C3H8'] = y_C3H8
2546
- if y_nC4H10 > 0: gas_comp['nC4H10'] = y_nC4H10
2547
- if y_iC4H10 > 0: gas_comp['iC4H10'] = y_iC4H10
2548
- if y_CO2 > 0: gas_comp['CO2'] = y_CO2
2549
- if y_N2 > 0: gas_comp['N2'] = y_N2
2550
- if y_H2S > 0: gas_comp['H2S'] = y_H2S
2551
- if y_H2 > 0: gas_comp['H2'] = y_H2
2551
+ gas_comp = {name: y for name, y in (
2552
+ ('CH4', y_CH4), ('C2H6', y_C2H6), ('C3H8', y_C3H8), ('nC4H10', y_nC4H10),
2553
+ ('iC4H10', y_iC4H10), ('CO2', y_CO2), ('N2', y_N2), ('H2S', y_H2S),
2554
+ ('H2', y_H2), ('He', y_He)) if y > 0}
2552
2555
 
2553
2556
  if not gas_comp:
2554
2557
  raise ValueError("At least one gas component must have non-zero mole fraction")