pyrestoolbox 3.1.0__tar.gz → 3.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/.github/workflows/build-wheels.yml +8 -7
  2. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/PKG-INFO +8 -8
  3. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/README.rst +6 -6
  4. pyrestoolbox-3.1.2/benchmark_rust_vs_python.py +220 -0
  5. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyproject.toml +2 -2
  6. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/simtools.rst +2 -2
  7. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/gas/gas.py +50 -22
  8. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/simtools/simtools.py +23 -18
  9. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/setup.cfg +1 -1
  10. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/setup.py +1 -1
  11. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/gwr.rs +1 -1
  12. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/zfactor/mod.rs +42 -32
  13. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/.gitignore +0 -0
  14. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/Cargo.lock +0 -0
  15. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/Cargo.toml +0 -0
  16. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/LICENSE +0 -0
  17. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/MANIFEST.in +0 -0
  18. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/ResToolbox/privacy_policy.md +0 -0
  19. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/build_pure_python.py +0 -0
  20. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/__init__.py +0 -0
  21. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/_accelerator.py +0 -0
  22. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/brine/__init__.py +0 -0
  23. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  24. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/brine/_lib_vle_engine.py +0 -0
  25. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/brine/brine.py +0 -0
  26. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/classes/__init__.py +0 -0
  27. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/classes/classes.py +0 -0
  28. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/constants/__init__.py +0 -0
  29. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/constants/constants.py +0 -0
  30. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/dca/__init__.py +0 -0
  31. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/dca/dca.py +0 -0
  32. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/brine.rst +0 -0
  33. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/changelist.rst +0 -0
  34. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/dca.rst +0 -0
  35. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/examples.ipynb +0 -0
  36. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/gas.rst +0 -0
  37. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/bot.png +0 -0
  38. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  39. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/bot_img.png +0 -0
  40. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  41. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  42. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/influence.png +0 -0
  43. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/properties_df.png +0 -0
  44. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/sgof.png +0 -0
  45. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/img/swof.png +0 -0
  46. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/layer.rst +0 -0
  47. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/library.rst +0 -0
  48. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/matbal.rst +0 -0
  49. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/nodal.rst +0 -0
  50. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
  51. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  52. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/oil.rst +0 -0
  53. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/recommend.rst +0 -0
  54. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/docs/sensitivity.rst +0 -0
  55. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/gas/__init__.py +0 -0
  56. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/layer/__init__.py +0 -0
  57. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/layer/layer.py +0 -0
  58. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/library/__init__.py +0 -0
  59. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/library/component_library.xlsx +0 -0
  60. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/library/library.py +0 -0
  61. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/matbal/__init__.py +0 -0
  62. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/matbal/matbal.py +0 -0
  63. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/nodal/__init__.py +0 -0
  64. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/nodal/nodal.py +0 -0
  65. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/oil/__init__.py +0 -0
  66. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/oil/oil.py +0 -0
  67. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/plyasunov/__init__.py +0 -0
  68. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
  69. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
  70. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  71. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/recommend/__init__.py +0 -0
  72. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/recommend/recommend.py +0 -0
  73. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/sensitivity/__init__.py +0 -0
  74. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  75. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/shared_fns/__init__.py +0 -0
  76. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  77. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/simtools/__init__.py +0 -0
  78. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/validate/__init__.py +0 -0
  79. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/pyrestoolbox/validate/validate.py +0 -0
  80. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/bessel.rs +0 -0
  81. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/critical_properties/mod.rs +0 -0
  82. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/dca/hyperbolic.rs +0 -0
  83. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/dca/mod.rs +0 -0
  84. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/dca/ransac.rs +0 -0
  85. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/gas_viscosity/mod.rs +0 -0
  86. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/lib.rs +0 -0
  87. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/matbal/mod.rs +0 -0
  88. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/matbal/objective.rs +0 -0
  89. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/oil/density.rs +0 -0
  90. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/oil/mod.rs +0 -0
  91. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/pseudopressure.rs +0 -0
  92. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/spycher_pruess/mod.rs +0 -0
  93. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/spycher_pruess/solubility.rs +0 -0
  94. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/alpha.rs +0 -0
  95. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/bip.rs +0 -0
  96. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/components.rs +0 -0
  97. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/eos.rs +0 -0
  98. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/flash.rs +0 -0
  99. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/fugacity.rs +0 -0
  100. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/k_init.rs +0 -0
  101. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/mod.rs +0 -0
  102. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vle/rachford_rice.rs +0 -0
  103. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/friction.rs +0 -0
  104. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/holdup_bb.rs +0 -0
  105. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/holdup_gray.rs +0 -0
  106. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/holdup_hb.rs +0 -0
  107. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/holdup_wg.rs +0 -0
  108. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/ift.rs +0 -0
  109. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/mod.rs +0 -0
  110. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/pvt_helpers.rs +0 -0
  111. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/segment_gas.rs +0 -0
  112. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/segment_oil.rs +0 -0
  113. {pyrestoolbox-3.1.0 → pyrestoolbox-3.1.2}/src/vlp/static_column.rs +0 -0
@@ -40,12 +40,12 @@ jobs:
40
40
  shell: bash
41
41
 
42
42
  - name: Build wheels via cibuildwheel
43
- uses: pypa/cibuildwheel@v2.21
43
+ uses: pypa/cibuildwheel@v2.23
44
44
  env:
45
45
  # Use maturin as the build frontend
46
46
  CIBW_BUILD_FRONTEND: "build"
47
- # Build for CPython 3.10-3.13 (drop 3.9 scipy 1.14+ requires 3.10)
48
- CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
47
+ # Build for CPython 3.8-3.13 (last 5+ years of Python releases)
48
+ CIBW_BUILD: "cp38-* 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
@@ -74,8 +74,9 @@ jobs:
74
74
  CIBW_TEST_COMMAND: >
75
75
  python -c "from pyrestoolbox._accelerator import get_status; s = get_status(); print(s); assert s['rust_available'], f'Rust not loaded: {s}'"
76
76
  CIBW_TEST_REQUIRES: "numpy"
77
- # Skip tests on Linux (scipy needs OpenBLAS absent in manylinux) and cross-arch
78
- CIBW_TEST_SKIP: "*-manylinux* *-macosx_x86_64"
77
+ # Skip tests on Linux (scipy needs OpenBLAS absent in manylinux), macOS (MPFR symbol issue),
78
+ # and cp38/cp39 (dependency ilt-inversion may not have matching wheels on PyPI yet)
79
+ CIBW_TEST_SKIP: "*-manylinux* *-macosx* cp38-* cp39-*"
79
80
 
80
81
  - uses: actions/upload-artifact@v4
81
82
  with:
@@ -98,10 +99,10 @@ jobs:
98
99
  uses: dtolnay/rust-toolchain@stable
99
100
 
100
101
  - name: Build wheels via cibuildwheel
101
- uses: pypa/cibuildwheel@v2.21
102
+ uses: pypa/cibuildwheel@v2.23
102
103
  env:
103
104
  CIBW_BUILD_FRONTEND: "build"
104
- CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
105
+ CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*"
105
106
  CIBW_ARCHS_MACOS: "x86_64"
106
107
  CIBW_TEST_COMMAND: >
107
108
  python -c "from pyrestoolbox._accelerator import get_status; s = get_status(); print(s); assert s['rust_available'], f'Rust not loaded: {s}'"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -20,7 +20,7 @@ Keywords: restoolbox,petroleum,reservoir
20
20
  Home-Page: https://github.com/mwburgoyne/pyResToolbox
21
21
  Author-email: "Mark W. Burgoyne" <mark.w.burgoyne@gmail.com>
22
22
  License: GPL-3.0-or-later
23
- Requires-Python: >=3.9
23
+ Requires-Python: >=3.8
24
24
  Description-Content-Type: text/x-rst; charset=UTF-8
25
25
  Project-URL: Homepage, https://github.com/mwburgoyne/pyResToolbox
26
26
 
@@ -203,7 +203,7 @@ A set of Gas-Oil relative permeability curves with the LET method
203
203
  >>> plt.grid('both')
204
204
  >>> plt.plot()
205
205
 
206
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/sgof.png
206
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/sgof.png
207
207
  :alt: SGOF Relative Permeability Curves
208
208
 
209
209
  Or a set of Water-Oil relative permeability curves with the Corey method
@@ -220,7 +220,7 @@ Or a set of Water-Oil relative permeability curves with the Corey method
220
220
  >>> plt.grid('both')
221
221
  >>> plt.plot()
222
222
 
223
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/swof.png
223
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/swof.png
224
224
  :alt: SWOF Relative Permeability Curves
225
225
 
226
226
  A set of dimensionless pressures for the constant terminal rate Van Everdingin & Hurst aquifer, along with an AQUTAB.INC export for use in ECLIPSE.
@@ -242,7 +242,7 @@ A set of dimensionless pressures for the constant terminal rate Van Everdingin &
242
242
  >>> plt.title('Constant Terminal Rate Solution')
243
243
  >>> plt.show()
244
244
 
245
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/influence.png
245
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/influence.png
246
246
  :alt: Constant Terminal Rate influence tables
247
247
 
248
248
  Or creating black oil table information for oil
@@ -295,7 +295,7 @@ Or creating black oil table information for oil
295
295
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
296
296
  Reservoir Water Viscosity: 0.3640686136171888 cP
297
297
 
298
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/bot.png
298
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot.png
299
299
  :alt: Black Oil Properties
300
300
 
301
301
  And gas
@@ -310,7 +310,7 @@ And gas
310
310
  >>> ...
311
311
  >>> plt.show()
312
312
 
313
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/dry_gas.png
313
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/dry_gas.png
314
314
  :alt: Dry Gas Properties
315
315
 
316
316
  With ability to generate Live Oil PVTO style table data as well
@@ -384,7 +384,7 @@ With ability to generate Live Oil PVTO style table data as well
384
384
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
385
385
  Reservoir Water Viscosity: 0.3640686136171888 cP
386
386
 
387
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/bot_PVTO.png
387
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot_PVTO.png
388
388
  :alt: Live Oil Properties
389
389
 
390
390
 
@@ -177,7 +177,7 @@ A set of Gas-Oil relative permeability curves with the LET method
177
177
  >>> plt.grid('both')
178
178
  >>> plt.plot()
179
179
 
180
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/sgof.png
180
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/sgof.png
181
181
  :alt: SGOF Relative Permeability Curves
182
182
 
183
183
  Or a set of Water-Oil relative permeability curves with the Corey method
@@ -194,7 +194,7 @@ Or a set of Water-Oil relative permeability curves with the Corey method
194
194
  >>> plt.grid('both')
195
195
  >>> plt.plot()
196
196
 
197
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/swof.png
197
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/swof.png
198
198
  :alt: SWOF Relative Permeability Curves
199
199
 
200
200
  A set of dimensionless pressures for the constant terminal rate Van Everdingin & Hurst aquifer, along with an AQUTAB.INC export for use in ECLIPSE.
@@ -216,7 +216,7 @@ A set of dimensionless pressures for the constant terminal rate Van Everdingin &
216
216
  >>> plt.title('Constant Terminal Rate Solution')
217
217
  >>> plt.show()
218
218
 
219
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/influence.png
219
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/influence.png
220
220
  :alt: Constant Terminal Rate influence tables
221
221
 
222
222
  Or creating black oil table information for oil
@@ -269,7 +269,7 @@ Or creating black oil table information for oil
269
269
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
270
270
  Reservoir Water Viscosity: 0.3640686136171888 cP
271
271
 
272
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/bot.png
272
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot.png
273
273
  :alt: Black Oil Properties
274
274
 
275
275
  And gas
@@ -284,7 +284,7 @@ And gas
284
284
  >>> ...
285
285
  >>> plt.show()
286
286
 
287
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/dry_gas.png
287
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/dry_gas.png
288
288
  :alt: Dry Gas Properties
289
289
 
290
290
  With ability to generate Live Oil PVTO style table data as well
@@ -358,7 +358,7 @@ With ability to generate Live Oil PVTO style table data as well
358
358
  Reservoir Water Compressibility: 2.930237693350768e-06 1/psi
359
359
  Reservoir Water Viscosity: 0.3640686136171888 cP
360
360
 
361
- .. image:: https://github.com/mwburgoyne/pyResToolbox/blob/main/pyrestoolbox/docs/img/bot_PVTO.png
361
+ .. image:: https://raw.githubusercontent.com/mwburgoyne/pyResToolbox/main/pyrestoolbox/docs/img/bot_PVTO.png
362
362
  :alt: Live Oil Properties
363
363
 
364
364
 
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Benchmark: Rust-accelerated vs pure-Python pyResToolbox functions.
4
+
5
+ Runs 8 test cases with BNS method, comparing results, accuracy and speedup.
6
+ Pure-Python runs in a subprocess with PYRESTOOLBOX_NO_RUST=1 (set before
7
+ import since the accelerator probes at import time).
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ import time
15
+
16
+ import numpy as np
17
+ from tabulate import tabulate
18
+
19
+
20
+ # ── Helper: run a test case in a subprocess ──────────────────────────────
21
+ def _run_in_subprocess(test_code: str, env_override: dict | None = None) -> dict:
22
+ """Execute test_code in a fresh Python process and return JSON result."""
23
+ env = os.environ.copy()
24
+ if env_override:
25
+ env.update(env_override)
26
+
27
+ wrapper = f"""
28
+ import json, time, sys, os
29
+ sys.path.insert(0, {os.getcwd()!r})
30
+ {test_code}
31
+ """
32
+ result = subprocess.run(
33
+ [sys.executable, "-c", wrapper],
34
+ capture_output=True, text=True, env=env, timeout=300
35
+ )
36
+ if result.returncode != 0:
37
+ return {"error": result.stderr.strip(), "elapsed": 0, "value": None}
38
+ try:
39
+ return json.loads(result.stdout.strip().split("\n")[-1])
40
+ except (json.JSONDecodeError, IndexError):
41
+ return {"error": f"Bad output: {result.stdout[:200]}", "elapsed": 0, "value": None}
42
+
43
+
44
+ # ── Test case definitions ────────────────────────────────────────────────
45
+
46
+ PREAMBLE = """
47
+ from pyrestoolbox import gas, oil
48
+ from pyrestoolbox.nodal import Completion, fbhp
49
+ from pyrestoolbox import simtools
50
+ from pyrestoolbox.classes import z_method, c_method
51
+ import numpy as np
52
+ import time
53
+ import json
54
+ """
55
+
56
+ # (label, function_call_string, test_code)
57
+ TESTS = [
58
+ (
59
+ "(a) Single Z-factor",
60
+ "gas.gas_z(p=3000, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)",
61
+ """
62
+ z = gas.gas_z(p=3000, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)
63
+ print(json.dumps({"elapsed": _elapsed, "value": float(z)}))
64
+ """,
65
+ ),
66
+ (
67
+ "(b) 1000x Z-factors",
68
+ "[gas.gas_z(p=p, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)\n for p in np.linspace(500, 5000, 1000)]",
69
+ """
70
+ pressures = np.linspace(500, 5000, 1000).tolist()
71
+ results = [gas.gas_z(p=p, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS) for p in pressures]
72
+ print(json.dumps({"elapsed": _elapsed, "value": float(np.mean(results))}))
73
+ """,
74
+ ),
75
+ (
76
+ "(c) Single viscosity",
77
+ "gas.gas_ug(p=3000, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)",
78
+ """
79
+ ug = gas.gas_ug(p=3000, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)
80
+ print(json.dumps({"elapsed": _elapsed, "value": float(ug)}))
81
+ """,
82
+ ),
83
+ (
84
+ "(d) 1000x viscosities",
85
+ "[gas.gas_ug(p=p, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS)\n for p in np.linspace(500, 5000, 1000)]",
86
+ """
87
+ pressures = np.linspace(500, 5000, 1000).tolist()
88
+ results = [gas.gas_ug(p=p, sg=0.7, degf=200, zmethod=z_method.BNS, cmethod=c_method.BNS) for p in pressures]
89
+ print(json.dumps({"elapsed": _elapsed, "value": float(np.mean(results))}))
90
+ """,
91
+ ),
92
+ (
93
+ "(e) Pseudopressure",
94
+ "gas.gas_dmp(p1=500, p2=4000, degf=200, sg=0.7, zmethod=z_method.BNS, cmethod=c_method.BNS)",
95
+ """
96
+ pp = gas.gas_dmp(p1=500, p2=4000, degf=200, sg=0.7, zmethod=z_method.BNS, cmethod=c_method.BNS)
97
+ print(json.dumps({"elapsed": _elapsed, "value": float(pp)}))
98
+ """,
99
+ ),
100
+ (
101
+ "(f) Gas outflow",
102
+ "fbhp(thp=500, completion=Completion(2.441, 10000, 100, 200),\n vlpmethod='WG', well_type='gas', qg_mmscfd=5, cgr=10,\n qw_bwpd=10, api=45, gsg=0.65)",
103
+ """
104
+ c = Completion(tid=2.441, length=10000, tht=100, bht=200)
105
+ bhp = fbhp(thp=500, completion=c, vlpmethod='WG', well_type='gas',
106
+ qg_mmscfd=5.0, cgr=10, qw_bwpd=10, api=45, gsg=0.65, wsg=1.07)
107
+ print(json.dumps({"elapsed": _elapsed, "value": float(bhp)}))
108
+ """,
109
+ ),
110
+ (
111
+ "(g) Oil outflow",
112
+ "fbhp(thp=200, completion=Completion(2.441, 8000, 100, 180),\n vlpmethod='HB', well_type='oil', qt_stbpd=2000, gor=800,\n wc=0.3, pb=2500, rsb=500, api=35, sgsp=0.65, gsg=0.65)",
113
+ """
114
+ c = Completion(tid=2.441, length=8000, tht=100, bht=180)
115
+ bhp = fbhp(thp=200, completion=c, vlpmethod='HB', well_type='oil',
116
+ qt_stbpd=2000, gor=800, wc=0.3, pb=2500, rsb=500,
117
+ api=35, sgsp=0.65, gsg=0.65)
118
+ print(json.dumps({"elapsed": _elapsed, "value": float(bhp)}))
119
+ """,
120
+ ),
121
+ (
122
+ "(h) CT Influence table",
123
+ "simtools.influence_tables(ReDs=[2, 5, 10],\n min_td=0.01, max_td=10, n_incr=10, M=7)",
124
+ """
125
+ tds, pds = simtools.influence_tables(ReDs=[2, 5, 10], min_td=0.01, max_td=10, n_incr=10, M=7, export=False)
126
+ val = sum(sum(pd) for pd in pds)
127
+ print(json.dumps({"elapsed": _elapsed, "value": float(val)}))
128
+ """,
129
+ ),
130
+ ]
131
+
132
+
133
+ def _wrap_timed(test_code: str) -> str:
134
+ """Wrap test code with timing. Replaces _elapsed placeholder."""
135
+ lines = test_code.strip().split("\n")
136
+ print_line = lines[-1]
137
+ setup_lines = "\n".join(lines[:-1])
138
+ return f"""
139
+ {PREAMBLE}
140
+ t0 = time.perf_counter()
141
+ {setup_lines}
142
+ _elapsed = time.perf_counter() - t0
143
+ {print_line}
144
+ """
145
+
146
+
147
+ def main():
148
+ print("=" * 78)
149
+ print(" pyResToolbox Benchmark: Rust-accelerated vs Pure Python (BNS)")
150
+ print("=" * 78)
151
+ print()
152
+
153
+ # Confirm Rust status
154
+ rust_check = _run_in_subprocess(
155
+ PREAMBLE + """
156
+ from pyrestoolbox._accelerator import get_status
157
+ s = get_status()
158
+ print(json.dumps({"elapsed": 0, "value": 1 if s['rust_available'] else 0}))
159
+ """
160
+ )
161
+ rust_avail = rust_check.get("value", 0) == 1
162
+ print(f" Rust acceleration available: {rust_avail}")
163
+ if not rust_avail:
164
+ print(" WARNING: Rust not available - benchmark will show Python vs Python")
165
+ print()
166
+
167
+ rows = []
168
+ for label, call_str, test_code in TESTS:
169
+ full_code = _wrap_timed(test_code)
170
+
171
+ # Run with Rust (default)
172
+ print(f" Running {label} (Rust)...", end="", flush=True)
173
+ r_rust = _run_in_subprocess(full_code)
174
+ if r_rust.get("error"):
175
+ print(f" ERROR: {r_rust['error'][:80]}")
176
+ rows.append([label, call_str, "ERROR", "", "", "", "", ""])
177
+ continue
178
+ print(f" {r_rust['elapsed']:.4f}s", flush=True)
179
+
180
+ # Run pure Python
181
+ print(f" Running {label} (Python)...", end="", flush=True)
182
+ r_py = _run_in_subprocess(full_code, env_override={"PYRESTOOLBOX_NO_RUST": "1"})
183
+ if r_py.get("error"):
184
+ print(f" ERROR: {r_py['error'][:80]}")
185
+ rows.append([label, call_str, f"{r_rust['elapsed']:.4f}", "ERROR",
186
+ f"{r_rust['value']:.6g}", "", "", ""])
187
+ continue
188
+ print(f" {r_py['elapsed']:.4f}s", flush=True)
189
+
190
+ speedup = r_py["elapsed"] / r_rust["elapsed"] if r_rust["elapsed"] > 1e-9 else float("inf")
191
+ v_rust, v_py = r_rust["value"], r_py["value"]
192
+ if v_rust is not None and v_py is not None and abs(v_py) > 1e-30:
193
+ rel_err = abs(v_rust - v_py) / abs(v_py)
194
+ accuracy = "Exact" if rel_err < 1e-12 else f"{rel_err:.2e}"
195
+ else:
196
+ accuracy = "N/A"
197
+
198
+ rows.append([
199
+ label,
200
+ call_str,
201
+ f"{r_rust['elapsed']:.4f}",
202
+ f"{r_py['elapsed']:.4f}",
203
+ f"{v_rust:.6g}",
204
+ f"{v_py:.6g}",
205
+ f"{speedup:.1f}x",
206
+ accuracy,
207
+ ])
208
+
209
+ print()
210
+ print(tabulate(
211
+ rows,
212
+ headers=["Test", "Function Call", "Rust (s)", "Python (s)",
213
+ "Rust Result", "Python Result", "Speedup", "Rel Error"],
214
+ tablefmt="pretty",
215
+ colalign=("left", "left", "right", "right", "right", "right", "right", "center"),
216
+ ))
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
@@ -4,12 +4,12 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pyrestoolbox"
7
- version = "3.1.0"
7
+ version = "3.1.2"
8
8
  description = "pyResToolbox - A collection of Reservoir Engineering Utilities"
9
9
  license = {text = "GPL-3.0-or-later"}
10
10
  authors = [{name = "Mark W. Burgoyne", email = "mark.w.burgoyne@gmail.com"}]
11
11
  keywords = ["restoolbox", "petroleum", "reservoir"]
12
- requires-python = ">=3.9"
12
+ requires-python = ">=3.8"
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  "Programming Language :: Rust",
@@ -146,7 +146,7 @@ pyrestoolbox.simtools.influence_tables
146
146
 
147
147
  .. code-block:: python
148
148
 
149
- influence_tables(ReDs, min_td = 0.01, max_td = 200, n_incr = 20, M = 8, export = False)-> tuple
149
+ influence_tables(ReDs, min_td = 0.01, max_td = 200, n_incr = 20, M = 7, export = False)-> tuple
150
150
 
151
151
  Solves Van Everdingin & Hurst Constant Terminal Rate solution via inverse Laplace transform and optionally writes out ECLIPSE styled AQUTAB include file.
152
152
 
@@ -175,7 +175,7 @@ Returns a tuple of;
175
175
  - Number of log transformed increments to split dimensionless time into. Default = 20
176
176
  * - M
177
177
  - int
178
- - Laplace invesrion accuracy. Higher = more accurate, but more time. Generally 6-12 is good range. Default = 8
178
+ - Laplace inversion accuracy. Higher = more accurate, but more time. Generally 6-12 is good range. Default = 7
179
179
  * - export
180
180
  - bool
181
181
  - Boolean value that controls whether an include file with 'INFLUENCE.INC' name is created. Default: False
@@ -536,32 +536,60 @@ def _cardano_cubic(c2, c1, c0, flag=0):
536
536
  return max(Zs)
537
537
  return Zs
538
538
 
539
- def _halley_cubic_vec(c2, c1, c0, max_iter=50, tol=1e-12):
539
+ def _halley_cubic_vec(c2, c1, c0, A=None, B=None, max_iter=50, tol=1e-12):
540
540
  """Vectorized Halley solver: solve Z^3+c2*Z^2+c1*Z+c0=0 for max root (vapor Z).
541
541
  c2, c1, c0 are 1D arrays of length N. Returns 1D array of Z values.
542
- Falls back to _cardano_cubic for any non-converged elements."""
543
- N = len(c2)
544
- Z = -c2 / 3.0
545
- f0 = Z**3 + c2 * Z**2 + c1 * Z + c0
546
- Z = np.where(f0 < 0, Z + 1.0, Z)
542
+ When A and B are provided, solves in Z* = Z - B space (per Aaron Zick's
543
+ reformulation) where all physical roots lie in (0, 1], giving more
544
+ robust convergence. Falls back to _cardano_cubic for any bad elements."""
545
+
546
+ if A is not None and B is not None:
547
+ # Z* = Z - B reformulation: Z*³ + d2·Z*² + d1·Z* + d0 = 0
548
+ # f*(0) = -2B² < 0, f*(1) = A >= 0, so largest root in (0, 1]
549
+ d2 = 4.0 * B - 1.0
550
+ d1 = A + 2.0 * B * (B - 2.0)
551
+ d0 = -2.0 * B * B
552
+
553
+ # Start at Z* = 1 (above largest root since f*(1) = A >= 0)
554
+ Zs = np.ones_like(c2)
555
+
556
+ for _ in range(max_iter):
557
+ f = Zs**3 + d2 * Zs**2 + d1 * Zs + d0
558
+ fp = 3.0 * Zs**2 + 2.0 * d2 * Zs + d1
559
+ fpp = 6.0 * Zs + 2.0 * d2
560
+ safe_fp = np.where(np.abs(fp) < 1e-30, 1e-30, fp)
561
+ dZ = f / safe_fp
562
+ denom = safe_fp - 0.5 * dZ * fpp
563
+ denom = np.where(np.abs(denom) < 1e-30, 1e-30, denom)
564
+ dZ = f / denom
565
+ Zs -= dZ
566
+ if np.max(np.abs(dZ)) < tol:
567
+ break
568
+
569
+ # Convert Z* -> Z; fallback to Cardano in Z space for bad elements
570
+ Z = Zs + B
571
+ f = Zs**3 + d2 * Zs**2 + d1 * Zs + d0
572
+ bad = (np.abs(f) > 1e-6) | (Zs <= 0.0)
573
+ else:
574
+ # Legacy path: solve directly in Z space with Cauchy upper bound
575
+ Z = 1.0 + np.maximum(np.abs(c2), np.maximum(np.abs(c1), np.abs(c0)))
576
+
577
+ for _ in range(max_iter):
578
+ f = Z**3 + c2 * Z**2 + c1 * Z + c0
579
+ fp = 3.0 * Z**2 + 2.0 * c2 * Z + c1
580
+ fpp = 6.0 * Z + 2.0 * c2
581
+ safe_fp = np.where(np.abs(fp) < 1e-30, 1e-30, fp)
582
+ dZ = f / safe_fp
583
+ denom = safe_fp - 0.5 * dZ * fpp
584
+ denom = np.where(np.abs(denom) < 1e-30, 1e-30, denom)
585
+ dZ = f / denom
586
+ Z -= dZ
587
+ if np.max(np.abs(dZ)) < tol:
588
+ break
547
589
 
548
- for _ in range(max_iter):
549
590
  f = Z**3 + c2 * Z**2 + c1 * Z + c0
550
- fp = 3.0 * Z**2 + 2.0 * c2 * Z + c1
551
- fpp = 6.0 * Z + 2.0 * c2
552
- # Protect against zero derivatives
553
- safe_fp = np.where(np.abs(fp) < 1e-30, 1e-30, fp)
554
- dZ = f / safe_fp
555
- denom = safe_fp - 0.5 * dZ * fpp
556
- denom = np.where(np.abs(denom) < 1e-30, 1e-30, denom)
557
- dZ = f / denom
558
- Z -= dZ
559
- if np.max(np.abs(dZ)) < tol:
560
- break
591
+ bad = (np.abs(f) > 1e-6) | (Z < 0.0)
561
592
 
562
- # Check residuals and fall back to Cardano for any bad elements
563
- f = Z**3 + c2 * Z**2 + c1 * Z + c0
564
- bad = np.abs(f) > 1e-6
565
593
  if np.any(bad):
566
594
  bad_idx = np.where(bad)[0]
567
595
  for idx in bad_idx:
@@ -766,7 +794,7 @@ def gas_z(
766
794
  c0 = -(A * B - B**2 - B**3)
767
795
 
768
796
  # Solve all cubics at once - get max (vapor) root
769
- Z_raw = _halley_cubic_vec(c2, c1_coeff, c0) # (N,)
797
+ Z_raw = _halley_cubic_vec(c2, c1_coeff, c0, A=A, B=B) # (N,)
770
798
 
771
799
  # Fugacity-based root selection for sub-critical conditions
772
800
  # When 3 real roots exist, the thermodynamically stable phase
@@ -64,6 +64,7 @@ import pandas as pd
64
64
  from tabulate import tabulate
65
65
  from ilt import gwr
66
66
  from mpmath import mp
67
+ from pyrestoolbox._accelerator import RUST_AVAILABLE, _rust_module
67
68
 
68
69
  from pyrestoolbox.classes import (kr_family, kr_table, vlp_method,
69
70
  z_method, c_method, pb_method, rs_method,
@@ -720,7 +721,7 @@ def influence_tables(
720
721
  min_td: float = 0.01,
721
722
  max_td: float = 200,
722
723
  n_incr: int = 20,
723
- M: int = 8,
724
+ M: int = 7,
724
725
  export: bool = False,
725
726
  ) -> Tuple:
726
727
  """ Returns a Tuple of;
@@ -734,7 +735,7 @@ def influence_tables(
734
735
  min_td: Minimum dimensionless time. Default = 0.01
735
736
  max_td: Maximum dimensionless time. Dfeault = 200
736
737
  n_incr: Number of increments to split dimensionless time into (log transformed), Default = 20
737
- M: Laplace invesrion accuracy. Higher = more accurate, but more time. Generally 6-12 is good range. Default = 8
738
+ M: Laplace inversion accuracy. Higher = more accurate, but more time. Generally 6-12 is good range. Default = 7
738
739
  export: Boolean value that controls whether an include file with 'INFLUENCE.INC' name is created. Default: False
739
740
  """
740
741
  if len(ReDs) == 0:
@@ -742,26 +743,30 @@ def influence_tables(
742
743
  if min(ReDs) <=1:
743
744
  raise ValueError("ReDs must all be strictly greater than 1.0")
744
745
 
745
- # Eq 23 from SPE 81428
746
- def laplace_Ps(s: float, ReD: float):
747
- x = mp.sqrt(s)
748
- # pre-calculate duplicated bessel function for greater efficiency
749
- i1sReD = mp.besseli(1, x * ReD)
750
- k1sReD = mp.besselk(1, x * ReD)
751
- numerator = k1sReD * mp.besseli(0, x) + i1sReD * mp.besselk(0, x)
752
- denominator = s ** 1.5 * (
753
- i1sReD * mp.besselk(1, x) - k1sReD * mp.besseli(1, x)
754
- )
755
- return numerator / denominator
756
-
757
746
  dtD = np.log(max_td / min_td) / n_incr
758
747
  tD = [np.exp(x * dtD + np.log(min_td)) for x in range(n_incr + 1)]
759
748
  tD = np.array(tD)
760
749
 
761
- pDs = []
762
- for ReD in ReDs:
763
- print("Calculating ReD = " + str(ReD))
764
- pDs.append(gwr(lambda s: laplace_Ps(s, ReD), tD, M))
750
+ # Rust fast path: Bessel + GWR entirely in Rust (no Python callbacks)
751
+ if RUST_AVAILABLE:
752
+ pDs = _rust_module.influence_tables_rust(tD.tolist(), list(ReDs), M)
753
+ else:
754
+ # Python fallback via ilt library
755
+ def laplace_Ps(s: float, ReD: float):
756
+ x = mp.sqrt(s)
757
+ # pre-calculate duplicated bessel function for greater efficiency
758
+ i1sReD = mp.besseli(1, x * ReD)
759
+ k1sReD = mp.besselk(1, x * ReD)
760
+ numerator = k1sReD * mp.besseli(0, x) + i1sReD * mp.besselk(0, x)
761
+ denominator = s ** 1.5 * (
762
+ i1sReD * mp.besselk(1, x) - k1sReD * mp.besseli(1, x)
763
+ )
764
+ return numerator / denominator
765
+
766
+ pDs = []
767
+ for ReD in ReDs:
768
+ print("Calculating ReD = " + str(ReD))
769
+ pDs.append(gwr(lambda s: laplace_Ps(s, ReD), tD, M))
765
770
 
766
771
  if export:
767
772
  inc_out = "---------------------------------------\n"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pyrestoolbox
3
- version = 3.1.0
3
+ version = 3.1.2
4
4
  author = Mark W. Burgoyne
5
5
  author_email = mark.w.burgoyne@gmail.com
6
6
  description = pyResToolbox - A collection of Reservoir Engineering Utilities
@@ -12,7 +12,7 @@ with open(os.path.join(ROOT, 'README.rst'), 'r', encoding='utf-8') as f:
12
12
  setup(
13
13
  name='pyrestoolbox',
14
14
  include_package_data=True,
15
- version='3.1.0', # Ideally should be same as your GitHub release tag version
15
+ version='3.1.2', # Ideally should be same as your GitHub release tag version
16
16
  packages=find_packages(exclude=['pyrestoolbox.tests', 'pyrestoolbox.tests.*']),
17
17
  description='pyResToolbox - A collection of Reservoir Engineering Utilities',
18
18
  license="GNU General Public License v3 or later (GPLv3+)",
@@ -511,7 +511,7 @@ fn gwr_single(
511
511
  // =========================================================================
512
512
 
513
513
  #[pyfunction]
514
- #[pyo3(signature = (td_array, red_array, m=8))]
514
+ #[pyo3(signature = (td_array, red_array, m=7))]
515
515
  pub fn influence_tables_rust(
516
516
  td_array: Vec<f64>,
517
517
  red_array: Vec<f64>,
@@ -288,37 +288,46 @@ fn cardano_cubic(c2: f64, c1: f64, c0: f64, flag: i32) -> f64 {
288
288
  }
289
289
  }
290
290
 
291
- /// Halley's method for finding max root of cubic z^3 + c2*z^2 + c1*z + c0 = 0
292
- fn halley_cubic(c2: f64, c1: f64, c0: f64) -> f64 {
293
- let mut z = -c2 / 3.0;
294
- let f0 = z * z * z + c2 * z * z + c1 * z + c0;
295
- if f0 < 0.0 {
296
- z += 1.0;
297
- }
291
+ /// Halley's method for finding max root of cubic z^3 + c2*z^2 + c1*z + c0 = 0,
292
+ /// using Z* = Z - B transformation (per Aaron Zick's reformulation).
293
+ /// For any standard cubic EOS with non-negative A, all physical Z* roots
294
+ /// lie in (0, 1], eliminating negative/huge roots and guaranteeing convergence.
295
+ /// Returns Z (not Z*), i.e. the B offset is added back before returning.
296
+ fn halley_cubic_zstar(a_mix: f64, b_mix: f64) -> f64 {
297
+ // Z* cubic: Z*³ + d2·Z*² + d1·Z* + d0 = 0
298
+ // where f*(0) = d0 = -2B² < 0 and f*(1) = A >= 0
299
+ let d2 = 4.0 * b_mix - 1.0;
300
+ let d1 = a_mix + 2.0 * b_mix * (b_mix - 2.0);
301
+ let d0 = -2.0 * b_mix * b_mix;
302
+
303
+ // Start at Z* = 1 where f* = A >= 0 (above largest root).
304
+ let mut zs = 1.0_f64;
298
305
 
299
306
  for _ in 0..50 {
300
- let f = z * z * z + c2 * z * z + c1 * z + c0;
301
- let fp = 3.0 * z * z + 2.0 * c2 * z + c1;
302
- let fpp = 6.0 * z + 2.0 * c2;
307
+ let f = zs * zs * zs + d2 * zs * zs + d1 * zs + d0;
308
+ let fp = 3.0 * zs * zs + 2.0 * d2 * zs + d1;
309
+ let fpp = 6.0 * zs + 2.0 * d2;
303
310
 
304
311
  let fp_safe = if fp.abs() < 1e-30 { 1e-30 } else { fp };
305
312
  let dz = f / fp_safe;
306
313
  let denom = fp_safe - 0.5 * dz * fpp;
307
314
  let denom_safe = if denom.abs() < 1e-30 { 1e-30 } else { denom };
308
315
  let dz_final = f / denom_safe;
309
- z -= dz_final;
316
+ zs -= dz_final;
310
317
  if dz_final.abs() < 1e-12 {
311
318
  break;
312
319
  }
313
320
  }
314
321
 
315
- // Verify convergence
316
- let f = z * z * z + c2 * z * z + c1 * z + c0;
317
- if f.abs() > 1e-6 {
318
- // Fallback to Cardano
322
+ // Verify convergence; fallback to Cardano in Z space if needed
323
+ let f = zs * zs * zs + d2 * zs * zs + d1 * zs + d0;
324
+ if f.abs() > 1e-6 || zs <= 0.0 {
325
+ let c2 = -(1.0 - b_mix);
326
+ let c1 = a_mix - 3.0 * b_mix * b_mix - 2.0 * b_mix;
327
+ let c0 = -(a_mix * b_mix - b_mix * b_mix - b_mix * b_mix * b_mix);
319
328
  return cardano_cubic(c2, c1, c0, 1);
320
329
  }
321
- z
330
+ zs + b_mix
322
331
  }
323
332
 
324
333
  fn bns_zfactor_core(
@@ -381,28 +390,29 @@ fn bns_zfactor_core(
381
390
  b_mix += zi[i] * bi[i];
382
391
  }
383
392
 
384
- // Cubic coefficients: Z^3 + c2*Z^2 + c1*Z + c0 = 0
385
- let c2 = -(1.0 - b_mix);
386
- let c1 = a_mix - 3.0 * b_mix * b_mix - 2.0 * b_mix;
387
- let c0 = -(a_mix * b_mix - b_mix * b_mix - b_mix * b_mix * b_mix);
388
-
389
- // Solve cubic - get max (vapor) root
390
- let z_max = halley_cubic(c2, c1, c0);
393
+ // Solve cubic in Z* = Z - B space (Aaron Zick reformulation)
394
+ let z_max = halley_cubic_zstar(a_mix, b_mix);
391
395
 
392
- // Check for 3-root case (fugacity-based root selection)
393
- let p_d = (3.0 * c1 - c2 * c2) / 3.0;
394
- let q_d = (2.0 * c2 * c2 * c2 - 9.0 * c2 * c1 + 27.0 * c0) / 27.0;
396
+ // Fugacity-based root selection for sub-critical conditions.
397
+ // Use Z* cubic discriminant to check for 3-root case.
398
+ let d2 = 4.0 * b_mix - 1.0;
399
+ let d1 = a_mix + 2.0 * b_mix * (b_mix - 2.0);
400
+ let d0 = -2.0 * b_mix * b_mix;
401
+ let p_d = (3.0 * d1 - d2 * d2) / 3.0;
402
+ let q_d = (2.0 * d2 * d2 * d2 - 9.0 * d2 * d1 + 27.0 * d0) / 27.0;
395
403
  let disc = q_d * q_d / 4.0 + p_d * p_d * p_d / 27.0;
396
404
 
397
405
  let z_selected = if disc < -1e-15 {
398
- // 3 real roots - find min root via deflation
399
- let b_q = c2 + z_max;
400
- let c_q = c1 + z_max * b_q;
406
+ // 3 real roots find min root via deflation in Z* space
407
+ let zs_max = z_max - b_mix; // convert back to Z*
408
+ let b_q = d2 + zs_max;
409
+ let c_q = d1 + zs_max * b_q;
401
410
  let det = (b_q * b_q - 4.0 * c_q).max(0.0);
402
- let z_min = (-b_q - det.sqrt()) / 2.0;
411
+ let zs_min = (-b_q - det.sqrt()) / 2.0;
412
+ let z_min = zs_min + b_mix; // convert to Z
403
413
 
404
- // Fugacity comparison (Gibbs criterion)
405
- if z_min > b_mix {
414
+ // Fugacity comparison (Gibbs criterion); Z > B ↔ Z* > 0
415
+ if zs_min > 0.0 {
406
416
  let sqrt2: f64 = std::f64::consts::SQRT_2;
407
417
  let s2p1 = 1.0 + sqrt2;
408
418
  let s2m1 = sqrt2 - 1.0;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes