pyrestoolbox 3.7.7__tar.gz → 3.8.0__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 (153) hide show
  1. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/.github/complexity-baseline.json +5 -8
  2. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/.github/workflows/build-wheels.yml +4 -4
  3. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/.github/workflows/tests.yml +4 -1
  4. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/.gitignore +3 -0
  5. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/Cargo.lock +1 -1
  6. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/Cargo.toml +1 -1
  7. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/PKG-INFO +17 -20
  8. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/README.rst +15 -17
  9. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/README.md +12 -5
  10. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/pyproject.toml +1 -1
  11. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/pyrestoolbox_mcp/server.py +87 -3
  12. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/tests/test_server.py +51 -2
  13. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyproject.toml +2 -3
  14. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/__init__.py +0 -2
  15. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/_accelerator.py +20 -0
  16. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/_lib_vle_engine.py +42 -23
  17. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/brine.py +147 -58
  18. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/pr_vphi.py +2 -2
  19. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/dca/dca.py +36 -22
  20. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/brine.rst +15 -15
  21. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/changelist.rst +67 -0
  22. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/dca.rst +1 -1
  23. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/gas.rst +96 -33
  24. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/layer.rst +2 -2
  25. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/nodal.rst +51 -48
  26. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/nodal_examples.ipynb +17 -19
  27. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +23 -26
  28. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/oil.rst +7 -7
  29. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/sensitivity.rst +1 -1
  30. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/simtools.rst +7 -1
  31. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/gas/_hydrate.py +48 -30
  32. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/gas/gas.py +106 -53
  33. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/layer/layer.py +1 -2
  34. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/matbal/matbal.py +2 -1
  35. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/nodal/nodal.py +295 -149
  36. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_compressibility.py +9 -16
  37. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_constants.py +12 -1
  38. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_correlations.py +39 -20
  39. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_density.py +2 -1
  40. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_harmonize.py +15 -29
  41. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_rate.py +2 -2
  42. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_tables.py +7 -6
  43. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_utils.py +17 -1
  44. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/plyasunov/plyasunov_model.py +12 -8
  45. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/shared_fns/shared_fns.py +62 -24
  46. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/_decks.py +16 -13
  47. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/_relperm.py +8 -1
  48. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/_vfp.py +41 -15
  49. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/setup.cfg +0 -1
  50. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/setup.py +0 -1
  51. pyrestoolbox-3.8.0/src/brine_visc/mod.rs +61 -0
  52. pyrestoolbox-3.8.0/src/brine_visc/salt.rs +149 -0
  53. pyrestoolbox-3.8.0/src/brine_visc/water.rs +157 -0
  54. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/gas_viscosity/mod.rs +6 -2
  55. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/lib.rs +5 -0
  56. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/oil/density.rs +25 -17
  57. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/oil/mod.rs +1 -1
  58. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/spycher_pruess/solubility.rs +70 -37
  59. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/constants.rs +5 -1
  60. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/holdup_bb.rs +8 -3
  61. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/holdup_gray.rs +2 -2
  62. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/march.rs +44 -42
  63. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/mod.rs +19 -4
  64. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/pvt_helpers.rs +57 -13
  65. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/static_column.rs +2 -1
  66. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/zfactor/mod.rs +16 -0
  67. pyrestoolbox-3.7.7/mcp/DEPLOYMENT.md +0 -53
  68. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/.github/scripts/check_complexity.py +0 -0
  69. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/LICENSE +0 -0
  70. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/MANIFEST.in +0 -0
  71. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/ResToolbox/privacy_policy.md +0 -0
  72. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/build_pure_python.py +0 -0
  73. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/clippy.toml +0 -0
  74. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/Dockerfile +0 -0
  75. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/pyrestoolbox_mcp/__init__.py +0 -0
  76. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/mcp/pyrestoolbox_mcp/serialize.py +0 -0
  77. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/__init__.py +0 -0
  78. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/_lib_dielectric.py +0 -0
  79. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/_lib_maoduan_viscosity.py +0 -0
  80. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/_lib_pitzer_params.py +0 -0
  81. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  82. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/appelo_volumes.py +0 -0
  83. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/iapws_viscosity.py +0 -0
  84. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/jones_dole_viscosity.py +0 -0
  85. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/kestin_nacl_viscosity.py +0 -0
  86. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/viscosity_route.py +0 -0
  87. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/brine/vphi_route.py +0 -0
  88. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/classes/__init__.py +0 -0
  89. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/classes/classes.py +0 -0
  90. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/constants/__init__.py +0 -0
  91. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/constants/constants.py +0 -0
  92. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/dca/__init__.py +0 -0
  93. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/examples.ipynb +0 -0
  94. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/bot.png +0 -0
  95. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  96. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
  97. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  98. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  99. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/influence.png +0 -0
  100. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
  101. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/sgof.png +0 -0
  102. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/img/swof.png +0 -0
  103. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/index.rst +0 -0
  104. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/library.rst +0 -0
  105. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/matbal.rst +0 -0
  106. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/plyasunov.rst +0 -0
  107. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/docs/recommend.rst +0 -0
  108. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/gas/__init__.py +0 -0
  109. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/layer/__init__.py +0 -0
  110. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/library/__init__.py +0 -0
  111. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/library/component_library.xlsx +0 -0
  112. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/library/library.py +0 -0
  113. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/matbal/__init__.py +0 -0
  114. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/nodal/__init__.py +0 -0
  115. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/__init__.py +0 -0
  116. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_pvt_class.py +0 -0
  117. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/oil/_separator.py +0 -0
  118. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/plyasunov/__init__.py +0 -0
  119. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
  120. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  121. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/recommend/__init__.py +0 -0
  122. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/recommend/recommend.py +0 -0
  123. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/sensitivity/__init__.py +0 -0
  124. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  125. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
  126. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/__init__.py +0 -0
  127. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/_aquifer.py +0 -0
  128. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
  129. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/simtools/simtools.py +0 -0
  130. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/validate/__init__.py +0 -0
  131. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/pyrestoolbox/validate/validate.py +0 -0
  132. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/bessel.rs +0 -0
  133. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/critical_properties/mod.rs +0 -0
  134. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/dca/hyperbolic.rs +0 -0
  135. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/dca/mod.rs +0 -0
  136. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/dca/ransac.rs +0 -0
  137. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/gwr.rs +0 -0
  138. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/matbal/mod.rs +0 -0
  139. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/matbal/objective.rs +0 -0
  140. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/pseudopressure.rs +0 -0
  141. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/spycher_pruess/mod.rs +0 -0
  142. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/alpha.rs +0 -0
  143. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/bip.rs +0 -0
  144. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/components.rs +0 -0
  145. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/eos.rs +0 -0
  146. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/flash.rs +0 -0
  147. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/fugacity.rs +0 -0
  148. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/k_init.rs +0 -0
  149. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/mod.rs +0 -0
  150. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vle/rachford_rice.rs +0 -0
  151. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/friction.rs +0 -0
  152. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/holdup_wg.rs +0 -0
  153. {pyrestoolbox-3.7.7 → pyrestoolbox-3.8.0}/src/vlp/ift.rs +0 -0
@@ -2,17 +2,15 @@
2
2
  "pyrestoolbox/brine/_lib_vle_engine.py::SWBinaryVLE.calc_gas_solubility": 16,
3
3
  "pyrestoolbox/brine/_lib_vle_engine.py::SWMultiComponentFlash.build_kij_matrix": 18,
4
4
  "pyrestoolbox/brine/_lib_vle_engine.py::SWMultiComponentFlash.calc_equilibrium": 24,
5
- "pyrestoolbox/brine/_lib_vle_engine.py::calc_gas_brine_equilibrium": 29,
5
+ "pyrestoolbox/brine/_lib_vle_engine.py::calc_gas_brine_equilibrium": 23,
6
6
  "pyrestoolbox/brine/brine.py::CO2_Brine_Mixture.__init__": 18,
7
- "pyrestoolbox/brine/brine.py::SoreideWhitson.__init__": 28,
7
+ "pyrestoolbox/brine/brine.py::SoreideWhitson.__init__": 27,
8
8
  "pyrestoolbox/brine/brine.py::SoreideWhitson._calc_properties": 19,
9
- "pyrestoolbox/brine/brine.py::brine_props": 19,
10
- "pyrestoolbox/dca/dca.py::fit_decline": 17,
11
- "pyrestoolbox/dca/dca.py::fit_decline_cum": 22,
9
+ "pyrestoolbox/brine/brine.py::brine_props": 17,
10
+ "pyrestoolbox/dca/dca.py::fit_decline_cum": 20,
12
11
  "pyrestoolbox/dca/dca.py::forecast": 16,
13
12
  "pyrestoolbox/dca/dca.py::hyp2_from_eur": 18,
14
13
  "pyrestoolbox/gas/_hydrate.py::gas_hydrate": 26,
15
- "pyrestoolbox/gas/gas.py::gas_ponz2p": 16,
16
14
  "pyrestoolbox/gas/gas.py::gas_tc_pc": 16,
17
15
  "pyrestoolbox/gas/gas.py::gas_ug": 16,
18
16
  "pyrestoolbox/matbal/matbal.py::gas_matbal": 26,
@@ -20,11 +18,10 @@
20
18
  "pyrestoolbox/nodal/nodal.py::Completion.__init__": 21,
21
19
  "pyrestoolbox/nodal/nodal.py::_bb_flow_pattern": 18,
22
20
  "pyrestoolbox/nodal/nodal.py::_prepare_flow_inputs": 16,
23
- "pyrestoolbox/nodal/nodal.py::ipr_curve": 27,
21
+ "pyrestoolbox/nodal/nodal.py::ipr_curve": 22,
24
22
  "pyrestoolbox/nodal/nodal.py::operating_point": 18,
25
23
  "pyrestoolbox/oil/_compressibility.py::oil_co": 16,
26
24
  "pyrestoolbox/oil/_correlations.py::oil_pbub": 16,
27
- "pyrestoolbox/oil/_harmonize.py::oil_harmonize": 16,
28
25
  "pyrestoolbox/shared_fns/shared_fns.py::validate_pe_inputs": 43,
29
26
  "pyrestoolbox/simtools/_decks.py::_crawl_deck_includes": 16,
30
27
  "pyrestoolbox/simtools/_decks.py::ix_extract_problem_cells": 41,
@@ -45,7 +45,7 @@ jobs:
45
45
  # Use maturin as the build frontend
46
46
  CIBW_BUILD_FRONTEND: "build"
47
47
  # Build for CPython 3.8-3.13 (last 5+ years of Python releases)
48
- CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*"
48
+ CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
49
49
  # Skip 32-bit and musl (uncommon for scientific Python)
50
50
  CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*"
51
51
  # maturin needs Rust in the build container
@@ -80,9 +80,9 @@ jobs:
80
80
  # missing OpenBLAS (this broke the v3.6.0 first build attempt - the
81
81
  # skip is load-bearing, do not remove it). Linux import + full-suite
82
82
  # coverage lives in .github/workflows/tests.yml instead.
83
- # Also skip macOS (MPFR symbol issue) and cp38/cp39 (dependency
83
+ # Also skip macOS (MPFR symbol issue) and cp39 (dependency
84
84
  # ilt-inversion may not have matching wheels on PyPI yet).
85
- CIBW_TEST_SKIP: "*-manylinux* *-macosx* cp38-* cp39-*"
85
+ CIBW_TEST_SKIP: "*-manylinux* *-macosx* cp39-*"
86
86
 
87
87
  - uses: actions/upload-artifact@v6
88
88
  with:
@@ -108,7 +108,7 @@ jobs:
108
108
  uses: pypa/cibuildwheel@v2.23
109
109
  env:
110
110
  CIBW_BUILD_FRONTEND: "build"
111
- CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*"
111
+ CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
112
112
  CIBW_ARCHS_MACOS: "x86_64"
113
113
  CIBW_TEST_COMMAND: >
114
114
  python -c "from pyrestoolbox._accelerator import get_status; s = get_status(); print(s); assert s['rust_available'], f'Rust not loaded: {s}'"
@@ -80,7 +80,7 @@ jobs:
80
80
  - name: Check cyclomatic complexity against baseline
81
81
  run: python3 .github/scripts/check_complexity.py
82
82
 
83
- # ── Rust lint ratchet (clippy clean; any new warning fails) ─────────
83
+ # ── Rust lint ratchet (clippy clean; any new warning fails) and unit tests ─
84
84
  rust-lint:
85
85
  name: Clippy
86
86
  runs-on: ubuntu-latest
@@ -96,3 +96,6 @@ jobs:
96
96
 
97
97
  - name: Clippy
98
98
  run: cargo clippy --all-targets -- -D warnings
99
+
100
+ - name: Rust unit tests
101
+ run: cargo test --release
@@ -57,3 +57,6 @@ pyrestoolbox-mcp-email.txt
57
57
  DEFECTS-FOUND-*.md
58
58
  PORT-SPEC-*.md
59
59
  *.zip
60
+
61
+ # Local primary-source library (copyrighted papers and its index) - never published
62
+ papers/
@@ -152,7 +152,7 @@ dependencies = [
152
152
 
153
153
  [[package]]
154
154
  name = "pyrestoolbox-native"
155
- version = "3.7.7"
155
+ version = "3.8.0"
156
156
  dependencies = [
157
157
  "pyo3",
158
158
  "rug",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pyrestoolbox-native"
3
- version = "3.7.7"
3
+ version = "3.8.0"
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.7.7
3
+ Version: 3.8.0
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+)
@@ -12,7 +12,6 @@ Requires-Dist: tabulate
12
12
  Requires-Dist: ilt-inversion
13
13
  Requires-Dist: mpmath
14
14
  Requires-Dist: gmpy2
15
- Requires-Dist: python-flint
16
15
  Requires-Dist: openpyxl
17
16
  License-File: LICENSE
18
17
  Summary: pyResToolbox - A collection of Reservoir Engineering Utilities
@@ -20,7 +19,7 @@ Keywords: restoolbox,petroleum,reservoir
20
19
  Home-Page: https://github.com/mwburgoyne/pyResToolbox
21
20
  Author-email: "Mark W. Burgoyne" <mark.w.burgoyne@gmail.com>
22
21
  License: GPL-3.0-or-later
23
- Requires-Python: >=3.8
22
+ Requires-Python: >=3.9
24
23
  Description-Content-Type: text/x-rst; charset=UTF-8
25
24
  Project-URL: Homepage, https://github.com/mwburgoyne/pyResToolbox
26
25
 
@@ -35,7 +34,7 @@ A collection of Reservoir Engineering Utilities
35
34
  This set of functions focuses on those that the author uses often while crafting programming solutions.
36
35
  These are the scripts that are often copy/pasted from previous work - sometimes slightly modified - resulting in a trail of slightly different versions over the years. Some attempt has been made here to make this implementation flexible enough such that it can be relied on as-is going forward.
37
36
 
38
- Note: Version 3.0 consolidates simulation-oriented functions under the simtools module, adds nodal analysis (VLP/IPR), VFP table generation, relative permeability curve fitting, and Eclipse METRIC unit support across all modules. Version 3.0.4 adds decline curve analysis, material balance, method recommendations, and sensitivity analysis.
37
+ Modules: gas, oil, brine, nodal (VLP/IPR, VFP tables), simtools (black-oil, PVTW, rel-perm, aquifer and deck tools), dca (decline curves), matbal (material balance), layer, library, recommend and sensitivity. Every module accepts Eclipse METRIC units with ``metric=True``; the full documentation ships inside the package, and ``pyrestoolbox.docs_dir()`` returns its location for offline reading.
39
38
 
40
39
  Includes functions to perform calculations including;
41
40
 
@@ -289,13 +288,11 @@ Or creating black oil table information for oil
289
288
  >>>
290
289
  >>> plt.tight_layout()
291
290
  >>> plt.show()
292
- Iteratively solving for Rsb fraction to use in order to harmonize user specified Pb and Rsb
293
-
294
- Stock Tank Oil Density: 52.09203539823009 lb/cuft
295
- Stock Tank Gas Density: 0.052046870460837856 lb/cuft
296
- Reservoir Water Density: 61.40223160167964 lb/cuft
291
+ Stock Tank Oil Density: 52.06448672566371 lb/cuft
292
+ Stock Tank Gas Density: 0.05204687045843109 lb/cuft
293
+ Reservoir Water Density: 61.37100825498743 lb/cuft
297
294
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
298
- Reservoir Water Viscosity: 0.3640686136171888 cP
295
+ Reservoir Water Viscosity: 0.36386395924173587 cP
299
296
 
300
297
  .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot.png
301
298
  :alt: Black Oil Properties
@@ -374,17 +371,17 @@ With ability to generate Live Oil PVTO style table data as well
374
371
  >>> ..
375
372
  >>> plt.show()
376
373
  Pb (psia): 4500
377
- Bob (rb/stb): 1.6072798403441817
378
- Rsb (mscf/stb): 1.2863705330979234
379
- Rsb Scaling Required: 0.9713981737449556
380
- Visob (cP): 0.3422139569449832
381
- Cob (1/psi): 5.711273668114706e-05
382
-
383
- Stock Tank Oil Density: 52.05522123893805 lb/cuft
384
- Stock Tank Gas Density: 0.052025361717109773 lb/cuft
385
- Reservoir Water Density: 61.40223160167964 lb/cuft
374
+ Bob (rb/stb): 1.5372158966466267
375
+ Rsb (mscf/stb): 1.137900924344287
376
+ Rsb Scaling Required: 1.0362710951888936
377
+ Visob (cP): 0.36663121233374113
378
+ Cob (1/psi): 2.0042955593519084e-05
379
+
380
+ Stock Tank Oil Density: 52.06448672566371 lb/cuft
381
+ Stock Tank Gas Density: 0.05204687045843109 lb/cuft
382
+ Reservoir Water Density: 61.37100825498743 lb/cuft
386
383
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
387
- Reservoir Water Viscosity: 0.3640686136171888 cP
384
+ Reservoir Water Viscosity: 0.36386395924173587 cP
388
385
 
389
386
  .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot_PVTO.png
390
387
  :alt: Live Oil Properties
@@ -9,7 +9,7 @@ A collection of Reservoir Engineering Utilities
9
9
  This set of functions focuses on those that the author uses often while crafting programming solutions.
10
10
  These are the scripts that are often copy/pasted from previous work - sometimes slightly modified - resulting in a trail of slightly different versions over the years. Some attempt has been made here to make this implementation flexible enough such that it can be relied on as-is going forward.
11
11
 
12
- Note: Version 3.0 consolidates simulation-oriented functions under the simtools module, adds nodal analysis (VLP/IPR), VFP table generation, relative permeability curve fitting, and Eclipse METRIC unit support across all modules. Version 3.0.4 adds decline curve analysis, material balance, method recommendations, and sensitivity analysis.
12
+ Modules: gas, oil, brine, nodal (VLP/IPR, VFP tables), simtools (black-oil, PVTW, rel-perm, aquifer and deck tools), dca (decline curves), matbal (material balance), layer, library, recommend and sensitivity. Every module accepts Eclipse METRIC units with ``metric=True``; the full documentation ships inside the package, and ``pyrestoolbox.docs_dir()`` returns its location for offline reading.
13
13
 
14
14
  Includes functions to perform calculations including;
15
15
 
@@ -263,13 +263,11 @@ Or creating black oil table information for oil
263
263
  >>>
264
264
  >>> plt.tight_layout()
265
265
  >>> plt.show()
266
- Iteratively solving for Rsb fraction to use in order to harmonize user specified Pb and Rsb
267
-
268
- Stock Tank Oil Density: 52.09203539823009 lb/cuft
269
- Stock Tank Gas Density: 0.052046870460837856 lb/cuft
270
- Reservoir Water Density: 61.40223160167964 lb/cuft
266
+ Stock Tank Oil Density: 52.06448672566371 lb/cuft
267
+ Stock Tank Gas Density: 0.05204687045843109 lb/cuft
268
+ Reservoir Water Density: 61.37100825498743 lb/cuft
271
269
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
272
- Reservoir Water Viscosity: 0.3640686136171888 cP
270
+ Reservoir Water Viscosity: 0.36386395924173587 cP
273
271
 
274
272
  .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot.png
275
273
  :alt: Black Oil Properties
@@ -348,17 +346,17 @@ With ability to generate Live Oil PVTO style table data as well
348
346
  >>> ..
349
347
  >>> plt.show()
350
348
  Pb (psia): 4500
351
- Bob (rb/stb): 1.6072798403441817
352
- Rsb (mscf/stb): 1.2863705330979234
353
- Rsb Scaling Required: 0.9713981737449556
354
- Visob (cP): 0.3422139569449832
355
- Cob (1/psi): 5.711273668114706e-05
356
-
357
- Stock Tank Oil Density: 52.05522123893805 lb/cuft
358
- Stock Tank Gas Density: 0.052025361717109773 lb/cuft
359
- Reservoir Water Density: 61.40223160167964 lb/cuft
349
+ Bob (rb/stb): 1.5372158966466267
350
+ Rsb (mscf/stb): 1.137900924344287
351
+ Rsb Scaling Required: 1.0362710951888936
352
+ Visob (cP): 0.36663121233374113
353
+ Cob (1/psi): 2.0042955593519084e-05
354
+
355
+ Stock Tank Oil Density: 52.06448672566371 lb/cuft
356
+ Stock Tank Gas Density: 0.05204687045843109 lb/cuft
357
+ Reservoir Water Density: 61.37100825498743 lb/cuft
360
358
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
361
- Reservoir Water Viscosity: 0.3640686136171888 cP
359
+ Reservoir Water Viscosity: 0.36386395924173587 cP
362
360
 
363
361
  .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot_PVTO.png
364
362
  :alt: Live Oil Properties
@@ -66,8 +66,7 @@ pyrestoolbox-mcp --transport streamable-http --host 0.0.0.0 --port 8000
66
66
 
67
67
  It is stateless (no session affinity needed behind a load balancer) and
68
68
  serves the MCP endpoint at `/mcp`. A `Dockerfile` is included; put an API
69
- gateway in front for authentication. See `DEPLOYMENT.md` for the platform
70
- team one-pager.
69
+ gateway in front for authentication.
71
70
 
72
71
  ## Tests
73
72
 
@@ -80,8 +79,16 @@ PYTHONPATH=<repo>:<repo>/mcp python3 -m pytest mcp/tests/ -q
80
79
  - Units default to oilfield (psia, deg F, ft, mD, cP); pass `metric: true`
81
80
  where the function supports it. The plyasunov module is SI (K, MPa).
82
81
  - The oil module is scalar-only; gas and brine functions accept lists.
83
- - Stateful classes (`GasPVT`, `CO2_Brine_Mixture`, `SoreideWhitson`,
84
- `DeclineResult`) are not exposed through `call` - the one-shot wrapper
85
- tools cover the common workflows.
82
+ - Gas rates are Mscf/d everywhere (sm3/d with `metric: true`).
83
+ - Functions that take objects are callable through `call` with those objects
84
+ written as JSON objects of their constructor keywords: `completion`
85
+ (`nodal.Completion`, with an optional `segments` list of `WellSegment`
86
+ objects), `reservoir`, `gas_pvt`, `oil_pvt`, `result` (the object a
87
+ `fit_decline` or `fit_ratio` call returned), `ratios`, and `func` as a
88
+ `'module.function'` name for `sensitivity.sweep`/`tornado`. For example
89
+ `call('nodal.fbhp', {"thp": 500, "well_type": "gas", "qg_mscfd": 5000,
90
+ "completion": {"tid": 2.441, "length": 10000, "tht": 100, "bht": 200}})`.
91
+ Classes themselves are not called directly; the one-shot wrapper tools
92
+ cover the common stateful workflows.
86
93
  - File-writing simtools functions keep their `export=False` defaults, so
87
94
  calls return table text rather than touching disk.
@@ -11,7 +11,7 @@ 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",
14
+ "mcp>=1.2,<2",
15
15
  "pyrestoolbox>=3.7",
16
16
  ]
17
17
  readme = "README.md"
@@ -53,6 +53,10 @@ mcp = FastMCP(
53
53
  "Fahrenheit value to `degf` with metric=True converts it twice and "
54
54
  "returns a wrong answer silently. Standard volumes are always at "
55
55
  "60 deg F and 14.696 psia, so metric sm3 is on that basis. "
56
+ "Gas rates are Mscf/d everywhere (sm3/d with metric=True). "
57
+ "Functions that take objects (Completion, Reservoir, GasPVT, OilPVT, "
58
+ "decline results) accept them through call() as JSON objects of their "
59
+ "constructor keywords; see the call tool description. "
56
60
  "Full module documentation is available as docs:// resources."
57
61
  ),
58
62
  )
@@ -147,8 +151,23 @@ def call(name: str, arguments: dict = {}) -> dict:
147
151
  Correlation methods are strings (e.g. "zmethod": "DAK"); an invalid
148
152
  method string returns an error listing the valid options. Gas and brine
149
153
  functions accept lists for pressure-like inputs; oil functions are
150
- scalar-only. Classes cannot be called here - use the one-shot wrapper
151
- tools (co2_brine_props, sw_brine_props, fit_and_forecast) instead.
154
+ scalar-only. Gas rates are Mscf/d (sm3/d with metric=True).
155
+
156
+ Object arguments are passed as JSON objects of their constructor keywords
157
+ and built here (describe the class for its keywords):
158
+ completion -> nodal.Completion (a "segments" list of objects -> WellSegment)
159
+ reservoir -> nodal.Reservoir
160
+ gas_pvt -> gas.GasPVT
161
+ oil_pvt -> oil.OilPVT
162
+ result -> dca.DeclineResult (forecast) or dca.RatioResult (ratio_forecast);
163
+ pass the object a fit_decline / fit_ratio call returned
164
+ ratios -> {name: RatioResult object}
165
+ func -> a 'module.function' name (sensitivity.sweep / tornado); objects
166
+ inside base_kwargs are built by the same rules
167
+ e.g. call('nodal.fbhp', {"thp": 500, "well_type": "gas", "qg_mscfd": 5000,
168
+ "completion": {"tid": 2.441, "length": 10000, "tht": 100, "bht": 200}}).
169
+ Classes themselves cannot be called here; one-shot wrappers
170
+ (co2_brine_props, sw_brine_props, fit_and_forecast) cover the common cases.
152
171
  """
153
172
  _, obj = _resolve(name)
154
173
  if inspect.isclass(obj):
@@ -159,10 +178,75 @@ def call(name: str, arguments: dict = {}) -> dict:
159
178
  )
160
179
  if not callable(obj):
161
180
  raise ValueError(f"{name} is not callable (it is {type(obj).__name__})")
162
- result = obj(**arguments)
181
+ result = obj(**_build_objects(obj, arguments))
163
182
  return {"result": to_jsonable(result)}
164
183
 
165
184
 
185
+ def _library_class(dotted: str):
186
+ module, cls = dotted.split(".")
187
+ return getattr(_get_module(module), cls)
188
+
189
+
190
+ # Keyword -> library class built from a JSON object. The library names these
191
+ # parameters consistently, so one table covers every function that takes them.
192
+ _OBJECT_PARAMS = {
193
+ "completion": "nodal.Completion",
194
+ "reservoir": "nodal.Reservoir",
195
+ "gas_pvt": "gas.GasPVT",
196
+ "oil_pvt": "oil.OilPVT",
197
+ }
198
+
199
+
200
+ def _build_objects(fn, arguments: dict) -> dict:
201
+ """Turn JSON objects in `arguments` into the library objects fn expects.
202
+
203
+ Raises ValueError naming the parameter and class when construction fails,
204
+ so an agent learns which keywords to fix instead of an AttributeError from
205
+ deep inside the library.
206
+ """
207
+ try:
208
+ annotations = {k: str(v.annotation) for k, v in inspect.signature(fn).parameters.items()}
209
+ except (TypeError, ValueError):
210
+ annotations = {}
211
+ out = {}
212
+ for key, value in arguments.items():
213
+ out[key] = _build_one(key, value, annotations.get(key, ""))
214
+ return out
215
+
216
+
217
+ def _build_one(key, value, annotation):
218
+ if key == "func" and isinstance(value, str):
219
+ _, fn = _resolve(value)
220
+ return fn
221
+ if key == "base_kwargs" and isinstance(value, dict):
222
+ return {k: _build_one(k, v, "") for k, v in value.items()}
223
+ if not isinstance(value, dict):
224
+ return value
225
+ if key == "ratios":
226
+ return {name: _construct("dca.RatioResult", key, spec) for name, spec in value.items()}
227
+ if key == "result":
228
+ dotted = "dca.RatioResult" if "RatioResult" in annotation else "dca.DeclineResult"
229
+ return _construct(dotted, key, value)
230
+ if key in _OBJECT_PARAMS:
231
+ if key == "completion" and isinstance(value.get("segments"), list):
232
+ value = dict(value, segments=[
233
+ _construct("nodal.WellSegment", "completion.segments", seg)
234
+ if isinstance(seg, dict) else seg for seg in value["segments"]])
235
+ return _construct(_OBJECT_PARAMS[key], key, value)
236
+ return value
237
+
238
+
239
+ def _construct(dotted, key, spec):
240
+ cls = _library_class(dotted)
241
+ try:
242
+ return cls(**spec)
243
+ except TypeError as exc:
244
+ raise ValueError(
245
+ f"Could not build {dotted} for '{key}' from {sorted(spec)}: {exc}. "
246
+ f"Use describe({dotted!r}) for its keywords."
247
+ ) from exc
248
+
249
+
166
250
  @mcp.tool()
167
251
  def recommend_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
168
252
  n2: float = 0, h2: float = 0, api: float | None = None,
@@ -106,6 +106,55 @@ def test_call_rejects_private():
106
106
  srv.call('gas._resolve_methods', {})
107
107
 
108
108
 
109
+ _COMPLETION = {'tid': 2.441, 'length': 10000, 'tht': 100, 'bht': 200}
110
+ _RESERVOIR = {'pr': 3000, 'degf': 200, 'k': 10, 'h': 50, 're': 1500, 'rw': 0.35, 'S': 2, 'D': 0.001}
111
+
112
+
113
+ def test_call_builds_completion_and_matches_doc_example():
114
+ r = srv.call('nodal.fbhp', {'thp': 500, 'completion': _COMPLETION, 'vlpmethod': 'HB',
115
+ 'well_type': 'gas', 'qg_mscfd': 5000, 'gsg': 0.65,
116
+ 'cgr': 10, 'qw_bwpd': 10, 'api': 45, 'oil_vis': 1.0})
117
+ assert abs(r['result'] - 961.594453485463) / 961.594453485463 < RTOL
118
+
119
+
120
+ def test_call_builds_segments_reservoir_and_gas_pvt():
121
+ segs = {'segments': [{'md': 5000, 'id': 2.441}, {'md': 5000, 'id': 2.441, 'deviation': 45}],
122
+ 'tht': 100, 'bht': 200}
123
+ r = srv.call('nodal.fbhp', {'thp': 500, 'completion': segs, 'vlpmethod': 'HB', 'well_type': 'gas',
124
+ 'qg_mscfd': 5000, 'gsg': 0.65, 'cgr': 10, 'qw_bwpd': 10,
125
+ 'api': 45, 'oil_vis': 1.0})
126
+ assert abs(r['result'] - 932.8973511781188) < 0.01
127
+ op = srv.call('nodal.operating_point', {'thp': 500, 'completion': _COMPLETION,
128
+ 'reservoir': _RESERVOIR, 'vlpmethod': 'HB',
129
+ 'well_type': 'gas', 'gsg': 0.65})['result']
130
+ assert abs(op['rate'] - 10578.1) < 0.2
131
+ assert srv.call('nodal.fthp', {'bhp': 1000, 'completion': _COMPLETION, 'well_type': 'gas',
132
+ 'qg_mscfd': 5000, 'gas_pvt': {'sg': 0.7, 'co2': 0.1}})['result'] > 14.7
133
+
134
+
135
+ def test_call_forecast_from_fit_result_and_ratio():
136
+ t = list(range(1, 25))
137
+ q = [1000 * 2.718281828 ** (-0.05 * x) for x in t]
138
+ fit = srv.call('dca.fit_decline', {'t': t, 'q': q, 'method': 'exponential'})['result']
139
+ ratio = {'method': 'linear', 'a': 2.0, 'b': 0.0, 'domain': 'time'}
140
+ fc = srv.call('dca.forecast', {'result': fit, 't_end': 60, 'dt': 30, 'ratios': {'gor': ratio}})['result']
141
+ assert all(abs(c - 2 * Q) < 1e-6 * Q for c, Q in zip(fc['secondary']['gor']['cum'], fc['Qcum']))
142
+ assert srv.call('dca.ratio_forecast', {'result': ratio, 'x': 10})['result'] == 2.0
143
+
144
+
145
+ def test_call_sensitivity_with_function_name_and_nested_objects():
146
+ r = srv.call('sensitivity.sweep', {'func': 'nodal.fbhp', 'vary_param': 'thp', 'vary_values': [300, 500],
147
+ 'base_kwargs': {'thp': 500, 'completion': _COMPLETION,
148
+ 'well_type': 'gas', 'qg_mscfd': 5000}})['result']
149
+ assert r['results'][1] > r['results'][0]
150
+
151
+
152
+ def test_call_bad_object_keyword_names_the_class():
153
+ with pytest.raises(ValueError, match="Could not build nodal.Completion for 'completion'"):
154
+ srv.call('nodal.fbhp', {'thp': 500, 'completion': {'tid': 2.441, 'lenght': 10000},
155
+ 'well_type': 'gas', 'qg_mscfd': 5000})
156
+
157
+
109
158
  # ----------------------------------------------------------- composite tools
110
159
 
111
160
  def test_recommend_methods_h2_mandatory_bns():
@@ -118,8 +167,8 @@ def test_recommend_methods_h2_mandatory_bns():
118
167
  def test_co2_brine_props_matches_doc_example():
119
168
  out = srv.co2_brine_props(pres=5000, temp=275, ppm=30000)
120
169
  # brine.rst worked example values
121
- assert abs(out['bw'][0] - 1.1091672843736888) < 1e-6
122
- assert abs(out['x'][0] - 0.02431225) < 1e-6
170
+ assert abs(out['bw'][0] - 1.1090966489093108) < 1e-6
171
+ assert abs(out['x'][0] - 0.02431474274865895) < 1e-6
123
172
  assert type(out['bw'][0]) is float # json-clean
124
173
 
125
174
 
@@ -4,12 +4,12 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrestoolbox"
7
- version = "3.7.7"
7
+ version = "3.8.0"
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"}]
11
11
  keywords = ["restoolbox", "petroleum", "reservoir"]
12
- requires-python = ">=3.8"
12
+ requires-python = ">=3.9"
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  "Programming Language :: Rust",
@@ -24,7 +24,6 @@ dependencies = [
24
24
  "ilt-inversion",
25
25
  "mpmath",
26
26
  "gmpy2",
27
- "python-flint",
28
27
  "openpyxl",
29
28
  ]
30
29
  readme = "README.rst"
@@ -11,8 +11,6 @@ These are the scripts that are often copy/pasted from previous work - sometimes
11
11
  in a trail of slightly different versions over the years. Some attempt has been made here to make this
12
12
  implementation flexible enough such that it can be relied on as-is going forward.
13
13
 
14
- Note: Version 2.x refactors functions into different modules, requiring separate imports.
15
-
16
14
  Usage: ``from pyrestoolbox import <module>`` then ``<module>.function(...)``.
17
15
  All inputs use oilfield units (psia, degF, ft, mD, cP) unless otherwise noted.
18
16
 
@@ -14,6 +14,25 @@ from pathlib import Path
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
+
18
+ def _warn_if_extension_stale(native):
19
+ """Warn when the compiled extension's version differs from the package's.
20
+
21
+ A stale local build otherwise runs old numerics silently (3.7.6 under a
22
+ 3.7.7 package was found on 25 September 2026).
23
+ """
24
+ try:
25
+ from pyrestoolbox import __version__ as pkg_version
26
+ except ImportError:
27
+ return
28
+ ext_version = getattr(native, '__version__', None)
29
+ if ext_version and pkg_version and ext_version != pkg_version:
30
+ import warnings
31
+ warnings.warn(
32
+ f"pyrestoolbox Rust extension is version {ext_version} but the package is "
33
+ f"{pkg_version}; rebuild the extension or set PYRESTOOLBOX_NO_RUST=1.",
34
+ RuntimeWarning, stacklevel=2)
35
+
17
36
  RUST_AVAILABLE: bool = False
18
37
  _rust_module = None
19
38
  _failure_reason: str = ""
@@ -139,6 +158,7 @@ else:
139
158
  RUST_AVAILABLE = True
140
159
  logger.debug("pyResToolbox Rust acceleration loaded successfully")
141
160
  _clear_sentinel()
161
+ _warn_if_extension_stale(_native)
142
162
 
143
163
  except ImportError as e:
144
164
  _failure_reason = f"ImportError: {e}"
@@ -2304,6 +2304,47 @@ class SWMultiComponentFlash:
2304
2304
  # =============================================================================
2305
2305
  # Main Multi-Component API
2306
2306
  # =============================================================================
2307
+ def _flash_with_feed_retries(flash, T_K, P_Pa, gas_vec, salinity_method):
2308
+ """Two-phase gas-brine flash, retrying with increasing water in the feed.
2309
+
2310
+ Returns the first result that is two-phase and converged on both sides;
2311
+ if none is, returns the last one with two_phase=False and a warning.
2312
+ """
2313
+ # The y_* inputs are the dry EQUILIBRIUM VAPOR composition. A
2314
+ # water-dominated feed strips the more-soluble gases into the liquid,
2315
+ # so the equilibrium vapor drifts off the requested composition (a
2316
+ # 95/5 water/gas feed depletes dry-basis CO2 from 0.73 to ~0.65 for
2317
+ # CH4+CO2 at 50 MPa, biasing x_CO2 ~10% low). A gas-excess feed pins
2318
+ # the vapor at the requested composition; single-gas results are
2319
+ # feed-independent (unique tie-line). Near the water boiling curve a
2320
+ # two-phase solution only exists for water-rich feeds (feed water must
2321
+ # lie between the liquid and vapor water mole fractions), so retry
2322
+ # with increasing water fraction until both flashes are two-phase.
2323
+ result = None
2324
+ for w_frac in (0.5, 0.9, 0.95, 0.98):
2325
+ z = np.concatenate(([w_frac], (1.0 - w_frac) * gas_vec))
2326
+ result = flash.calc_equilibrium(T_K, P_Pa, z,
2327
+ salinity_method=salinity_method)
2328
+ V_aq = result.get('V_aq', np.nan)
2329
+ V_na = result.get('V_na', np.nan)
2330
+ if (result.get('converged_aq', False)
2331
+ and result.get('converged_na', False)
2332
+ and 1e-6 < V_aq < 1 - 1e-6
2333
+ and 1e-6 < V_na < 1 - 1e-6
2334
+ and result['x_aq'][0] > 0.5):
2335
+ break
2336
+ else:
2337
+ # Every feed came back single-phase or unconverged, so x_aq/y_na are
2338
+ # the flash feed itself, not an equilibrium. Say so.
2339
+ warnings.warn(
2340
+ f"SoreideWhitson: no two-phase gas-brine split found at {P_Pa / 1e5:.1f} bar, "
2341
+ f"{T_K - 273.15:.1f} degC; the returned compositions are the flash feed, "
2342
+ f"not an equilibrium (outside the model's two-phase region).",
2343
+ RuntimeWarning, stacklevel=3)
2344
+ result['two_phase'] = False
2345
+ return result
2346
+
2347
+
2307
2348
  def calc_gas_brine_equilibrium(
2308
2349
  salinity_wt_pct: float,
2309
2350
  temperature_F: float,
@@ -2406,29 +2447,7 @@ def calc_gas_brine_equilibrium(
2406
2447
  framework=framework,
2407
2448
  salinity_method=salinity_method)
2408
2449
 
2409
- # The y_* inputs are the dry EQUILIBRIUM VAPOR composition. A
2410
- # water-dominated feed strips the more-soluble gases into the liquid,
2411
- # so the equilibrium vapor drifts off the requested composition (a
2412
- # 95/5 water/gas feed depletes dry-basis CO2 from 0.73 to ~0.65 for
2413
- # CH4+CO2 at 50 MPa, biasing x_CO2 ~10% low). A gas-excess feed pins
2414
- # the vapor at the requested composition; single-gas results are
2415
- # feed-independent (unique tie-line). Near the water boiling curve a
2416
- # two-phase solution only exists for water-rich feeds (feed water must
2417
- # lie between the liquid and vapor water mole fractions), so retry
2418
- # with increasing water fraction until both flashes are two-phase.
2419
- result = None
2420
- for w_frac in (0.5, 0.9, 0.95, 0.98):
2421
- z = np.concatenate(([w_frac], (1.0 - w_frac) * gas_vec))
2422
- result = flash.calc_equilibrium(T_K, P_Pa, z,
2423
- salinity_method=salinity_method)
2424
- V_aq = result.get('V_aq', np.nan)
2425
- V_na = result.get('V_na', np.nan)
2426
- if (result.get('converged_aq', False)
2427
- and result.get('converged_na', False)
2428
- and 1e-6 < V_aq < 1 - 1e-6
2429
- and 1e-6 < V_na < 1 - 1e-6
2430
- and result['x_aq'][0] > 0.5):
2431
- break
2450
+ result = _flash_with_feed_retries(flash, T_K, P_Pa, gas_vec, salinity_method)
2432
2451
 
2433
2452
  x_gas = {}
2434
2453
  for i, name in enumerate(comp_names):