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.
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/workflows/tests.yml +26 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/Cargo.lock +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/Cargo.toml +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/PKG-INFO +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/README.rst +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/Dockerfile +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyproject.toml +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/server.py +24 -14
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/tests/test_server.py +41 -10
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyproject.toml +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_salting_library.py +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_vle_engine.py +73 -70
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/brine.py +31 -10
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/pr_vphi.py +10 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/vphi_route.py +2 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/classes/classes.py +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/constants/constants.py +3 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/brine.rst +59 -13
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/changelist.rst +18 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/examples.ipynb +17 -17
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/gas.rst +184 -39
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/index.rst +1 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/matbal.rst +4 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal.rst +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal_examples.ipynb +12 -12
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/oil.rst +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/recommend.rst +10 -4
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/_hydrate.py +7 -3
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/gas.py +142 -124
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/matbal/matbal.py +6 -3
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/nodal/nodal.py +6 -7
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/plyasunov_model.py +1 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/recommend/recommend.py +11 -6
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/shared_fns/shared_fns.py +4 -12
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/validate/validate.py +2 -2
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/bessel.rs +5 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/critical_properties/mod.rs +12 -6
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/gas_viscosity/mod.rs +32 -25
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/pseudopressure.rs +14 -14
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/bip.rs +23 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/components.rs +9 -4
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/k_init.rs +13 -1
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/zfactor/mod.rs +67 -53
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/complexity-baseline.json +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/scripts/check_complexity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/.gitignore +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/LICENSE +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/MANIFEST.in +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/build_pure_python.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/clippy.toml +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/README.md +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/mcp/pyrestoolbox_mcp/serialize.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/_accelerator.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_dielectric.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_maoduan_viscosity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/_lib_pitzer_params.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/appelo_volumes.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/iapws_viscosity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/jones_dole_viscosity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/kestin_nacl_viscosity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/brine/viscosity_route.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/dca/dca.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/plyasunov.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_compressibility.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_constants.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_correlations.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_density.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_harmonize.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_pvt_class.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_rate.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_separator.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_tables.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/oil/_utils.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_aquifer.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_decks.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_relperm.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/_vfp.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/simtools/simtools.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/setup.cfg +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/setup.py +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/salt.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/brine_visc/water.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/gwr.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/lib.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/oil/density.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/oil/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/spycher_pruess/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/spycher_pruess/solubility.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/flash.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/constants.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/march.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.8.2 → pyrestoolbox-3.8.4}/src/vlp/pvt_helpers.rs +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrestoolbox
|
|
3
|
-
Version: 3.8.
|
|
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
|
|
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
|
|
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
|
|
@@ -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,<
|
|
15
|
-
"pyrestoolbox>=3.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
386
|
-
mcp.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1553
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
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,
|
|
1567
|
-
|
|
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 *
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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")
|