pyrestoolbox 3.1.4__tar.gz → 3.2.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.
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/PKG-INFO +1 -1
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyproject.toml +1 -1
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/brine/_lib_vle_engine.py +6 -3
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/brine/brine.py +15 -17
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/dca/dca.py +47 -2
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/changelist.rst +39 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/layer/layer.py +48 -63
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/nodal/nodal.py +20 -7
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_correlations.py +4 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_separator.py +1 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/recommend/recommend.py +8 -2
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/sensitivity/sensitivity.py +11 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/simtools/simtools.py +44 -99
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/validate/validate.py +21 -5
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/mod.rs +23 -14
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/.gitignore +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/Cargo.lock +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/Cargo.toml +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/LICENSE +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/MANIFEST.in +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/README.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/benchmark_rust_vs_python.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/build_pure_python.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/_accelerator.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/classes/classes.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/brine.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/examples.ipynb +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/gas.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/matbal.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/nodal.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/oil.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/recommend.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/gas/gas.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/matbal/matbal.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_compressibility.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_constants.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_density.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_harmonize.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_pvt_class.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_rate.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_tables.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/oil/_utils.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/setup.cfg +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/setup.py +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/bessel.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/critical_properties/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/gas_viscosity/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/gwr.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/lib.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/oil/density.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/oil/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/pseudopressure.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/spycher_pruess/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/spycher_pruess/solubility.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/bip.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/components.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/flash.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/k_init.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/holdup_hb.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/pvt_helpers.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/segment_gas.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/segment_oil.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/vlp/static_column.rs +0 -0
- {pyrestoolbox-3.1.4 → pyrestoolbox-3.2.0}/src/zfactor/mod.rs +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyrestoolbox"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.2.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"}]
|
|
@@ -2100,9 +2100,12 @@ class SWMultiComponentFlash:
|
|
|
2100
2100
|
z = np.asarray(z, dtype=float)
|
|
2101
2101
|
z = z / np.sum(z)
|
|
2102
2102
|
|
|
2103
|
-
# Rust acceleration path — gamma is always passed explicitly
|
|
2104
|
-
#
|
|
2105
|
-
#
|
|
2103
|
+
# Rust acceleration path — gamma is always passed explicitly.
|
|
2104
|
+
# The Rust entry point trusts the caller-supplied gamma and never
|
|
2105
|
+
# substitutes its own S&W Eq 8 ks fallback. framework='proposed'
|
|
2106
|
+
# specialised ks models (Dubessy CO2, Akinfiev H2S, Li HC,
|
|
2107
|
+
# Mao-Duan N2, Duan-Sun CO2) are applied in self.calc_gamma()
|
|
2108
|
+
# on the Python side.
|
|
2106
2109
|
if _RUST_AVAILABLE:
|
|
2107
2110
|
try:
|
|
2108
2111
|
gamma_arr = np.asarray(gamma, dtype=float) if gamma is not None \
|
|
@@ -456,6 +456,7 @@ class CO2_Brine_Mixture():
|
|
|
456
456
|
.Rs : CO2 Saturated Brine solution gas ratio (sm3/sm3 or scf/stb), relative to standard conditions
|
|
457
457
|
.Cf_usat : Brine undersaturated compressibility (1/Bar or 1/psi). The compressibility with constant Rs
|
|
458
458
|
.Cf_sat : Brine saturated compressibility (1/Bar or 1/psi). The compressibility with reducing Rs under depletion
|
|
459
|
+
.converged: True if the Spycher-Pruess fugacity iteration reached tolerance, False otherwise
|
|
459
460
|
|
|
460
461
|
Usage example for 5000 psia x 275 deg F and 3% NaCl brine:
|
|
461
462
|
mix = brine.CO2_Brine_Mixture(pres = 5000, temp = 275, ppm = 30000, metric = False)
|
|
@@ -523,6 +524,7 @@ class CO2_Brine_Mixture():
|
|
|
523
524
|
self.Cf_sat = None # Saturated brine compressibility (1/Bar or 1/psi). Compressibility with changing Rs
|
|
524
525
|
self.CO2_sat = False # Flag to determine if in P-T range for saturated liquid CO2 K values
|
|
525
526
|
self.repeat = False # Flag to trigger repeat of calculations depending on whether CO2 is saturated liquid phase
|
|
527
|
+
self.converged = True # Spycher-Pruess fugacity iteration convergence flag
|
|
526
528
|
#self.EzrokhiDenA = None # Ezrokhi coefficient array for density (to be calculated with ezrokhi() function)
|
|
527
529
|
#self.EzrokhiVisB = None # Ezrokhi coefficient array for viscosity (to be calculated with ezrokhi() function)
|
|
528
530
|
self.Rs_STD = None # Dissolved CO2 remaining at standard conditions (sm3/sm3)
|
|
@@ -1125,6 +1127,7 @@ class CO2_Brine_Mixture():
|
|
|
1125
1127
|
iternum += 1
|
|
1126
1128
|
|
|
1127
1129
|
if err > EPS:
|
|
1130
|
+
self.converged = False
|
|
1128
1131
|
import warnings
|
|
1129
1132
|
warnings.warn(
|
|
1130
1133
|
f"Spycher CO2-brine iteration did not converge in {iternum} iterations "
|
|
@@ -1266,11 +1269,14 @@ class CO2_Brine_Mixture():
|
|
|
1266
1269
|
|
|
1267
1270
|
# -- Correcting brine density for dissolved CO2, JE Garcia, LBNL Report# 49023, Oct 2011, "Density of Aqueous Solutions of CO2"
|
|
1268
1271
|
def garciaDensity(rhoBRnoCO2, tKel, pBar, ppm, xCO2, MwB, MwG):
|
|
1272
|
+
# Algebraic reformulation of Garcia Eq 18: original form
|
|
1273
|
+
# (1 + mRat*xRat) / (vPhi*xRat/MwB + 1/rhoBRnoCO2) with xRat = xCO2/(1-xCO2)
|
|
1274
|
+
# has a singularity at xCO2 -> 1. Multiplying numerator and denominator by
|
|
1275
|
+
# (1-xCO2)*MwB removes xRat entirely and gives the same result.
|
|
1269
1276
|
xNotCO2 = 1.0 - xCO2 #--Brine (H20+Salt) Mole Fraction
|
|
1270
|
-
xRat = xCO2 / xNotCO2 #--Mole Fraction Ratio, Gas/Brine
|
|
1271
1277
|
mRat = MwG / MwB #--Mole Weight Ratio , Gas/Brine
|
|
1272
1278
|
vPhi = partMolVol(tKel) #--Apparent Molar Volume of Dissolved CO2
|
|
1273
|
-
return (
|
|
1279
|
+
return MwB * (xNotCO2 + mRat * xCO2) / (vPhi * xCO2 + MwB * xNotCO2 / rhoBRnoCO2)
|
|
1274
1280
|
|
|
1275
1281
|
# Correct CO2 free brine viscosity for dissolved CO2
|
|
1276
1282
|
# Using approach from "Viscosity Models and Effects of Dissolved CO2", Akand W. Islam and Eric S. Carlson (Jul 2012), Energy Fuels 2012, 26, 8, 5330�5336, https://doi.org/10.1021/ef3006228
|
|
@@ -1798,23 +1804,15 @@ class SoreideWhitson:
|
|
|
1798
1804
|
vphi_eff += yi * _plyasunov_V_phi(gas_ply, tKel, Mpa)
|
|
1799
1805
|
mw_eff += yi * _plyasunov_gas_mw(gas_ply)
|
|
1800
1806
|
|
|
1801
|
-
# Garcia Eq. 18 in g/cm3
|
|
1802
|
-
#
|
|
1807
|
+
# Garcia Eq. 18 in g/cm3, algebraically reformulated to remove the
|
|
1808
|
+
# x1 -> 0 singularity. Multiplying top/bot of
|
|
1809
|
+
# (1 + x2*M2/(M1*x1)) / (x2*V_phi/(M1*x1) + 1/rho1)
|
|
1810
|
+
# by M1*x1 gives the equivalent, non-singular form below.
|
|
1803
1811
|
x1 = 1.0 - self.x_total
|
|
1804
1812
|
M1 = MWWAT
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
warnings.warn(
|
|
1809
|
-
f"Gas mole fraction x_total={self.x_total:.4f} is too high for "
|
|
1810
|
-
"Garcia density mixing rule. Returning unsaturated brine density.",
|
|
1811
|
-
RuntimeWarning, stacklevel=2
|
|
1812
|
-
)
|
|
1813
|
-
rho_gas_brine_gcc = rho_brine_gcc
|
|
1814
|
-
else:
|
|
1815
|
-
numerator = 1.0 + self.x_total * mw_eff / (M1 * x1)
|
|
1816
|
-
denominator = self.x_total * vphi_eff / (M1 * x1) + 1.0 / rho_brine_gcc
|
|
1817
|
-
rho_gas_brine_gcc = numerator / denominator
|
|
1813
|
+
numerator = M1 * x1 + self.x_total * mw_eff
|
|
1814
|
+
denominator = self.x_total * vphi_eff + M1 * x1 / rho_brine_gcc
|
|
1815
|
+
rho_gas_brine_gcc = numerator / denominator
|
|
1818
1816
|
else:
|
|
1819
1817
|
rho_gas_brine_gcc = rho_brine_gcc
|
|
1820
1818
|
|
|
@@ -59,6 +59,13 @@ if _RUST_AVAILABLE:
|
|
|
59
59
|
from pyrestoolbox import _native as _rust
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _fmt_array(a, name):
|
|
63
|
+
if a is None:
|
|
64
|
+
return f"{name}=None"
|
|
65
|
+
arr = np.asarray(a)
|
|
66
|
+
return f"{name}=ndarray(shape={arr.shape}, dtype={arr.dtype})"
|
|
67
|
+
|
|
68
|
+
|
|
62
69
|
@dataclass
|
|
63
70
|
class DeclineResult:
|
|
64
71
|
"""Result from decline curve fitting.
|
|
@@ -97,6 +104,18 @@ class DeclineResult:
|
|
|
97
104
|
uptime_mean: Optional[float] = None
|
|
98
105
|
uptime_history: Optional[np.ndarray] = None
|
|
99
106
|
|
|
107
|
+
def __repr__(self):
|
|
108
|
+
scalars = (
|
|
109
|
+
f"method={self.method!r}, qi={self.qi}, di={self.di}, b={self.b}, "
|
|
110
|
+
f"a={self.a}, m={self.m}, r_squared={self.r_squared}, "
|
|
111
|
+
f"uptime_mean={self.uptime_mean}"
|
|
112
|
+
)
|
|
113
|
+
return (
|
|
114
|
+
f"DeclineResult({scalars}, "
|
|
115
|
+
f"{_fmt_array(self.residuals, 'residuals')}, "
|
|
116
|
+
f"{_fmt_array(self.uptime_history, 'uptime_history')})"
|
|
117
|
+
)
|
|
118
|
+
|
|
100
119
|
|
|
101
120
|
@dataclass
|
|
102
121
|
class ForecastResult:
|
|
@@ -122,6 +141,16 @@ class ForecastResult:
|
|
|
122
141
|
eur: float
|
|
123
142
|
secondary: Optional[dict] = None
|
|
124
143
|
|
|
144
|
+
def __repr__(self):
|
|
145
|
+
sec = 'None' if self.secondary is None else f"{{{', '.join(self.secondary.keys())}}}"
|
|
146
|
+
return (
|
|
147
|
+
f"ForecastResult("
|
|
148
|
+
f"{_fmt_array(self.t, 't')}, "
|
|
149
|
+
f"{_fmt_array(self.q, 'q')}, "
|
|
150
|
+
f"{_fmt_array(self.Qcum, 'Qcum')}, "
|
|
151
|
+
f"eur={self.eur}, secondary={sec})"
|
|
152
|
+
)
|
|
153
|
+
|
|
125
154
|
|
|
126
155
|
@dataclass
|
|
127
156
|
class RatioResult:
|
|
@@ -152,6 +181,13 @@ class RatioResult:
|
|
|
152
181
|
r_squared: float = 0.0
|
|
153
182
|
residuals: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
154
183
|
|
|
184
|
+
def __repr__(self):
|
|
185
|
+
return (
|
|
186
|
+
f"RatioResult(method={self.method!r}, a={self.a}, b={self.b}, "
|
|
187
|
+
f"c={self.c}, domain={self.domain!r}, r_squared={self.r_squared}, "
|
|
188
|
+
f"{_fmt_array(self.residuals, 'residuals')})"
|
|
189
|
+
)
|
|
190
|
+
|
|
155
191
|
|
|
156
192
|
def arps_rate(qi, di, b, t):
|
|
157
193
|
"""Arps decline rate.
|
|
@@ -287,10 +323,13 @@ def duong_cum(qi, a, m, t):
|
|
|
287
323
|
if np.any(t <= 0):
|
|
288
324
|
raise ValueError("Time must be positive for Duong model")
|
|
289
325
|
|
|
290
|
-
# Generate fine time grid for integration
|
|
326
|
+
# Generate fine time grid for integration. Lower bound must be strictly
|
|
327
|
+
# less than ti to give np.linspace an ascending range, otherwise trapezoid
|
|
328
|
+
# integrates over a descending axis and returns negative cumulative.
|
|
291
329
|
results = np.zeros_like(t, dtype=float)
|
|
292
330
|
for i, ti in enumerate(t):
|
|
293
|
-
|
|
331
|
+
lower = min(0.001, ti * 0.001)
|
|
332
|
+
t_fine = np.linspace(lower, ti, max(500, int(ti * 10)))
|
|
294
333
|
q_fine = qi * t_fine ** (-m) * np.exp(a / (1.0 - m) * (t_fine ** (1.0 - m) - 1.0))
|
|
295
334
|
results[i] = np.trapezoid(q_fine, t_fine)
|
|
296
335
|
|
|
@@ -922,6 +961,12 @@ def forecast(result, t_end, dt=1.0, q_min=0.0, uptime=1.0, ratios=None):
|
|
|
922
961
|
-------
|
|
923
962
|
ForecastResult
|
|
924
963
|
"""
|
|
964
|
+
if dt <= 0:
|
|
965
|
+
raise ValueError(f"dt must be positive, got {dt}")
|
|
966
|
+
if t_end <= 0:
|
|
967
|
+
raise ValueError(f"t_end must be positive, got {t_end}")
|
|
968
|
+
if not (0.0 < uptime <= 1.0):
|
|
969
|
+
raise ValueError(f"uptime must be in (0, 1], got {uptime}")
|
|
925
970
|
t = np.arange(dt, t_end + dt / 2, dt)
|
|
926
971
|
|
|
927
972
|
if result.method == 'duong':
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
Changelist in 3.2.0:
|
|
2
|
+
|
|
3
|
+
- **DCA bug fixes**:
|
|
4
|
+
|
|
5
|
+
- ``dca.duong_cum`` — fixed linspace-inversion bug where cumulative volume integrated over a descending axis for ``t < 0.001`` (small-t inputs) and returned a negative value. Lower bound of the integration is now ``min(0.001, t * 0.001)`` so the grid is always ascending.
|
|
6
|
+
- ``dca.forecast`` — now validates ``dt > 0``, ``t_end > 0`` and ``uptime`` in ``(0, 1]`` at entry with clear ``ValueError`` messages. Previously ``dt = 0`` raised an opaque ``ZeroDivisionError`` and out-of-range ``uptime`` was silently accepted.
|
|
7
|
+
|
|
8
|
+
- **Rachford-Rice solver consolidation**: ``simtools.rr_solver`` is now a thin wrapper that delegates to the canonical ``pyrestoolbox.brine._lib_vle_engine.rr_solver`` (Nielsen & Lia 2022). Removes ~100 lines of duplicated iterative code plus the dead ``ensure_numpy_array`` helper. Inputs validated for length/sum before delegation; behaviour is preserved (``EPS_T=1e-15``, ``max_iter=100``).
|
|
9
|
+
|
|
10
|
+
- **Rust Sechenov fallback guard**: ``src/vle/mod.rs::flash_tp_rust`` no longer silently substitutes its S&W Eq 8 ``ks`` fallback when a caller passes all-ones ``gamma`` with ``salinity > 0``. The caller-supplied ``gamma`` is now always trusted, eliminating any risk that Python ``framework='proposed'`` calls bypass the specialised Dubessy/Akinfiev/Li/Mao-Duan/Duan-Sun ``ks`` models. ``calc_equilibrium_rust`` retains the S&W Eq 8 path but now carries a prominent doc warning. Python path unchanged (Python always passes the correct ``gamma``).
|
|
11
|
+
|
|
12
|
+
- **Convergence flag on ``CO2_Brine_Mixture``**: New ``.converged`` attribute on the class. ``True`` after a successful Spycher-Pruess fugacity iteration, ``False`` when the 100-iteration limit is hit (matching the existing ``RuntimeWarning``). Lets downstream callers detect non-convergence programmatically.
|
|
13
|
+
|
|
14
|
+
- **``sensitivity.tornado`` robustness**: Raises ``ValueError`` if ``base_result`` is not finite (NaN/Inf), and if any ``ranges[param]`` has ``lo > hi``. Previously returned ``nan`` or ``inf`` sensitivities that silently corrupted tornado plots.
|
|
15
|
+
|
|
16
|
+
- **``layer`` module dedup**: Five copies of the EXP/LANG dispatch (B-clamp, flow-fraction evaluation) consolidated into three private helpers (``_clamp_b``, ``_b_max``, ``_flow_fraction_at_x``). No behaviour change.
|
|
17
|
+
|
|
18
|
+
- **``recommend`` module docstrings**: ``sg`` on ``recommend_gas_methods`` and ``well_type`` on ``recommend_vlp_method`` are currently unused by the decision logic. Docstrings now flag them as reserved for future rules. Signatures preserved for backward compatibility.
|
|
19
|
+
|
|
20
|
+
- 701 validation tests (up from 696 in 3.1.5).
|
|
21
|
+
|
|
22
|
+
Changelist in 3.1.5:
|
|
23
|
+
|
|
24
|
+
- **Agent-friendly UX**:
|
|
25
|
+
|
|
26
|
+
- ``validate_methods`` invalid-method errors now list valid options (e.g. ``Invalid zmethod: 'NOSUCH'. Valid options: ['DAK', 'HY', 'WYW', 'BNS', 'BUR']``). New ``validate_choice`` helper used by all ``nodal`` public entry points (``fbhp``, ``outflow_curve``, ``ipr_curve``, ``operating_point``) to validate ``well_type``.
|
|
27
|
+
- ``simtools.zip_check_sim_deck`` and ``simtools.ix_extract_problem_cells`` accept a ``non_interactive=True`` kwarg that raises a ``ValueError`` instead of prompting on ``input()``. Safe for scripts and agents without stdin.
|
|
28
|
+
- ``simtools.make_vfpinj``/``make_vfpprod`` BHP-failure warnings now use ``warnings.warn`` instead of ``print``.
|
|
29
|
+
- ``DeclineResult``, ``ForecastResult``, ``RatioResult`` ``__repr__`` now summarise array fields as ``ndarray(shape=..., dtype=...)`` so printing a result doesn't flood agent transcripts.
|
|
30
|
+
- ``nodal`` unit-validation errors (``Reservoir.__init__``, ``WellSegment.__init__``) echo the user's original value and unit (e.g. ``got -1 m``) rather than the post-conversion internal number.
|
|
31
|
+
|
|
32
|
+
- **Release-blocking numerical fixes**:
|
|
33
|
+
|
|
34
|
+
- **Garcia CO2-brine density**: Algebraic reformulation of Eq 18 (``brine.garciaDensity`` and ``SoreideWhitson._calc_properties``) removes the ``xCO2 → 1`` singularity. Finite rho at ``xCO2 = 1`` equals ``MwG / vPhi``. Mathematically identical to the old formula for ``xCO2 < 1`` (regression: 1e-12).
|
|
35
|
+
- **``oil.Rs_velarde`` at atmospheric ``pb``**: Now returns ``0.0`` when ``pb <= psc`` instead of emitting NaN from a ``0/0`` division.
|
|
36
|
+
- **``oil.sg_evolved_gas`` silent NaN**: Now calls ``validate_pe_inputs`` at entry; zero pressure or zero ``sg_sp`` raises ``ValueError`` instead of returning NaN.
|
|
37
|
+
|
|
38
|
+
- 696 validation tests (up from 691 in 3.1.4).
|
|
39
|
+
|
|
1
40
|
Changelist in 3.1.4:
|
|
2
41
|
|
|
3
42
|
- **Tier 4 brine improvements**: Adaptive VLE damping in ``flash_tp`` and ``calc_water_content_with_kij`` (replaces fixed 0.7/0.9 factors), ``V2_inf`` cached via ``functools.lru_cache(256)`` in Plyasunov model, ``build_kij_matrix`` cached per ``(T_K, mode)`` on VLE engine instance. Rust VLE flash updated with matching adaptive damping. All three brine models (``CH4_Brine``, ``CO2_Brine_Mixture``, ``SoreideWhitson``) now accept ``p``/``degf``/``wt`` and ``pres``/``temp``/``ppm`` parameter aliases.
|
|
@@ -41,6 +41,35 @@ import numpy.typing as npt
|
|
|
41
41
|
|
|
42
42
|
from pyrestoolbox.shared_fns import bisect_solve
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
_B_MIN = 1e-6
|
|
46
|
+
_B_MAX_EXP = 709
|
|
47
|
+
_B_MAX_LANG = 25000
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _b_max(method: str) -> float:
|
|
51
|
+
"""Upper bookend for B in bisection for the given Lorenz method."""
|
|
52
|
+
return _B_MAX_LANG if method == "LANG" else _B_MAX_EXP
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _clamp_b(B: float, method: str) -> float:
|
|
56
|
+
"""Clamp B to [_B_MIN, _b_max(method)]."""
|
|
57
|
+
return min(max(B, _B_MIN), _b_max(method))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _flow_fraction_at_x(B: float, x: float, method: str) -> float:
|
|
61
|
+
"""Flow fraction from best phi_h fraction x, given B and method.
|
|
62
|
+
|
|
63
|
+
EXP: (1 - exp(-B*x)) / (1 - exp(-B))
|
|
64
|
+
LANG: (VL * x) / (PL + x) with PL = 1/B, VL = PL + 1
|
|
65
|
+
"""
|
|
66
|
+
if method == "LANG":
|
|
67
|
+
PL = 1 / B
|
|
68
|
+
VL = PL + 1
|
|
69
|
+
return (VL * x) / (PL + x)
|
|
70
|
+
return (1 - np.exp(-B * x)) / (1 - np.exp(-B))
|
|
71
|
+
|
|
72
|
+
|
|
44
73
|
def lorenz2b(lorenz: float, lrnz_method: str = "EXP") -> float:
|
|
45
74
|
""" Returns B-factor that characterizes the Lorenz function
|
|
46
75
|
Lorenz: Lorenz coefficient (0-1)
|
|
@@ -63,37 +92,22 @@ def lorenz2b(lorenz: float, lrnz_method: str = "EXP") -> float:
|
|
|
63
92
|
return 0.0 # Homogeneous — no heterogeneity
|
|
64
93
|
|
|
65
94
|
if lorenz < 0.000333:
|
|
66
|
-
|
|
67
|
-
if method == "LANG":
|
|
68
|
-
B = 1 / 1000
|
|
69
|
-
return B
|
|
95
|
+
return 1 / 1000 if method == "LANG" else 2 / 1000
|
|
70
96
|
if lorenz > 0.997179125528914:
|
|
71
|
-
|
|
72
|
-
if method == "LANG":
|
|
73
|
-
B = 25000
|
|
74
|
-
return B
|
|
97
|
+
return _b_max(method)
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if method == "LANG":
|
|
79
|
-
hi = 25000
|
|
80
|
-
lo = 0.000001
|
|
99
|
+
hi = _b_max(method)
|
|
100
|
+
lo = _B_MIN
|
|
81
101
|
args = (lorenz, method)
|
|
82
102
|
|
|
83
103
|
def LorenzErr(args, B):
|
|
84
104
|
lorenz, method = args
|
|
85
|
-
B =
|
|
86
|
-
if method == "
|
|
87
|
-
B = min(B, 709)
|
|
88
|
-
err = 2 * ((1 / (np.exp(B) - 1)) - (1 / B)) + 1 - lorenz
|
|
89
|
-
else:
|
|
90
|
-
B = min(B, 25000)
|
|
105
|
+
B = _clamp_b(B, method)
|
|
106
|
+
if method == "LANG":
|
|
91
107
|
PL = 1 / B
|
|
92
108
|
VL = PL + 1
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
) * 2 - lorenz
|
|
96
|
-
return err
|
|
109
|
+
return (VL - PL * VL * np.log(VL) + PL * VL * np.log(PL) - 0.5) * 2 - lorenz
|
|
110
|
+
return 2 * ((1 / (np.exp(B) - 1)) - (1 / B)) + 1 - lorenz
|
|
97
111
|
|
|
98
112
|
rtol = 0.0000001
|
|
99
113
|
return bisect_solve(args, LorenzErr, lo, hi, rtol)
|
|
@@ -113,16 +127,12 @@ def lorenzfromb(B: float, lrnz_method: str = "EXP") -> float:
|
|
|
113
127
|
method = lrnz_method.upper()
|
|
114
128
|
if B <= 0:
|
|
115
129
|
return 0.0 # B=0 means homogeneous (Lc=0)
|
|
116
|
-
B =
|
|
130
|
+
B = _clamp_b(B, method)
|
|
117
131
|
if method == "LANG":
|
|
118
|
-
B = min(B, 25000)
|
|
119
132
|
PL = 1 / B
|
|
120
133
|
VL = PL + 1
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
B = min(B, 709)
|
|
124
|
-
L = 2 * (1 / (np.exp(B) - 1) - (1 / B)) + 1
|
|
125
|
-
return L
|
|
134
|
+
return (VL - PL * VL * np.log(VL) + PL * VL * np.log(PL) - 0.5) * 2
|
|
135
|
+
return 2 * (1 / (np.exp(B) - 1) - (1 / B)) + 1
|
|
126
136
|
|
|
127
137
|
|
|
128
138
|
def lorenz_from_flow_fraction(
|
|
@@ -155,23 +165,15 @@ def lorenz_from_flow_fraction(
|
|
|
155
165
|
return lorenzfromb(B, method)
|
|
156
166
|
|
|
157
167
|
# Set bookends and first guess of B
|
|
158
|
-
hi =
|
|
159
|
-
lo =
|
|
168
|
+
hi = _B_MAX_EXP
|
|
169
|
+
lo = _B_MIN
|
|
160
170
|
args = (kh_frac, phih_frac, method)
|
|
161
171
|
|
|
162
172
|
def BErr(args, B):
|
|
163
173
|
kh_frac, phih_frac, method = args
|
|
164
174
|
method = method.upper()
|
|
165
|
-
B =
|
|
166
|
-
|
|
167
|
-
B = min(B, 709)
|
|
168
|
-
err = (1 - np.exp(-B * phih_frac)) / (1 - np.exp(-B)) - kh_frac
|
|
169
|
-
else:
|
|
170
|
-
B = min(B, 25000)
|
|
171
|
-
PL = 1 / B
|
|
172
|
-
VL = PL + 1
|
|
173
|
-
err = (VL * phih_frac) / (PL + phih_frac) - kh_frac
|
|
174
|
-
return err
|
|
175
|
+
B = _clamp_b(B, method)
|
|
176
|
+
return _flow_fraction_at_x(B, phih_frac, method) - kh_frac
|
|
175
177
|
|
|
176
178
|
rtol = 0.0000001
|
|
177
179
|
B = bisect_solve(args, BErr, lo, hi, rtol)
|
|
@@ -203,16 +205,8 @@ def lorenz_2_flow_frac(
|
|
|
203
205
|
if B < 0: # Need to calculate B
|
|
204
206
|
B = lorenz2b(lorenz=lorenz, lrnz_method=lrnz_method)
|
|
205
207
|
|
|
206
|
-
B =
|
|
207
|
-
|
|
208
|
-
B = min(B, 709)
|
|
209
|
-
fraction = (1 - np.exp(-B * phih_frac)) / (1 - np.exp(-B))
|
|
210
|
-
else:
|
|
211
|
-
B = min(B, 25000)
|
|
212
|
-
PL = 1 / B
|
|
213
|
-
VL = PL + 1
|
|
214
|
-
fraction = (VL * phih_frac) / (PL + phih_frac)
|
|
215
|
-
return fraction
|
|
208
|
+
B = _clamp_b(B, method)
|
|
209
|
+
return _flow_fraction_at_x(B, phih_frac, method)
|
|
216
210
|
|
|
217
211
|
|
|
218
212
|
def lorenz_2_layers(
|
|
@@ -255,11 +249,7 @@ def lorenz_2_layers(
|
|
|
255
249
|
if B < 0: # Need to calculate B
|
|
256
250
|
B = lorenz2b(lorenz=lorenz, lrnz_method=lrnz_method)
|
|
257
251
|
|
|
258
|
-
B =
|
|
259
|
-
if method == "EXP":
|
|
260
|
-
B = min(B, 709)
|
|
261
|
-
else:
|
|
262
|
-
B = min(B, 25000)
|
|
252
|
+
B = _clamp_b(B, method)
|
|
263
253
|
|
|
264
254
|
user_layers = False
|
|
265
255
|
if len(phi_h_fracs) > 1:
|
|
@@ -280,12 +270,7 @@ def lorenz_2_layers(
|
|
|
280
270
|
sumkh = []
|
|
281
271
|
|
|
282
272
|
for layer in phih:
|
|
283
|
-
|
|
284
|
-
sumkh.append((1 - np.exp(-B * layer)) / (1 - np.exp(-B)))
|
|
285
|
-
else:
|
|
286
|
-
PL = 1 / B
|
|
287
|
-
VL = PL + 1
|
|
288
|
-
sumkh.append((VL * layer) / (PL + layer))
|
|
273
|
+
sumkh.append(_flow_fraction_at_x(B, layer, method))
|
|
289
274
|
|
|
290
275
|
kh = (
|
|
291
276
|
np.array([sumkh[i] - sumkh[i - 1] for i in range(1, len(sumkh))])
|
|
@@ -47,7 +47,7 @@ import warnings
|
|
|
47
47
|
import numpy as np
|
|
48
48
|
|
|
49
49
|
from pyrestoolbox.classes import vlp_method, class_dic
|
|
50
|
-
from pyrestoolbox.validate import validate_methods
|
|
50
|
+
from pyrestoolbox.validate import validate_methods, validate_choice
|
|
51
51
|
from pyrestoolbox.shared_fns import bisect_solve, validate_pe_inputs
|
|
52
52
|
from pyrestoolbox.constants import (BAR_TO_PSI, PSI_TO_BAR, degc_to_degf, degf_to_degc,
|
|
53
53
|
M_TO_FT, FT_TO_M, MM_TO_IN, IN_TO_MM,
|
|
@@ -306,16 +306,19 @@ class WellSegment:
|
|
|
306
306
|
def __init__(self, md, id, deviation=0, roughness=None, metric=False):
|
|
307
307
|
if roughness is None:
|
|
308
308
|
roughness = 0.01524 if metric else 0.0006 # 0.0006 in = 0.01524 mm
|
|
309
|
+
_md_in, _id_in, _rough_in = md, id, roughness
|
|
310
|
+
_unit_len = 'm' if metric else 'ft'
|
|
311
|
+
_unit_dia = 'mm' if metric else 'in'
|
|
309
312
|
if metric:
|
|
310
313
|
md = md * M_TO_FT
|
|
311
314
|
id = id * MM_TO_IN
|
|
312
315
|
roughness = roughness * MM_TO_IN
|
|
313
316
|
if md <= 0:
|
|
314
|
-
raise ValueError(f"Measured depth md must be positive, got {
|
|
317
|
+
raise ValueError(f"Measured depth md must be positive, got {_md_in} {_unit_len}")
|
|
315
318
|
if id <= 0:
|
|
316
|
-
raise ValueError(f"Internal diameter id must be positive, got {
|
|
319
|
+
raise ValueError(f"Internal diameter id must be positive, got {_id_in} {_unit_dia}")
|
|
317
320
|
if roughness < 0:
|
|
318
|
-
raise ValueError(f"Roughness must be non-negative, got {
|
|
321
|
+
raise ValueError(f"Roughness must be non-negative, got {_rough_in} {_unit_dia}")
|
|
319
322
|
if not (0 <= deviation <= 90):
|
|
320
323
|
raise ValueError(f"Deviation must be between 0 and 90 degrees, got {deviation}")
|
|
321
324
|
self.md = md # stored in ft
|
|
@@ -573,6 +576,9 @@ class Reservoir:
|
|
|
573
576
|
metric: If True, inputs in metric units (barsa, degC, m, day/sm3). Default False.
|
|
574
577
|
"""
|
|
575
578
|
def __init__(self, pr, degf, k, h, re, rw, S=0, D=0, metric=False):
|
|
579
|
+
# Preserve original user-facing values for error messages
|
|
580
|
+
_pr_in, _degf_in, _h_in, _re_in, _rw_in = pr, degf, h, re, rw
|
|
581
|
+
_unit_len = 'm' if metric else 'ft'
|
|
576
582
|
if metric:
|
|
577
583
|
pr = pr * BAR_TO_PSI
|
|
578
584
|
degf = degc_to_degf(degf)
|
|
@@ -585,11 +591,14 @@ class Reservoir:
|
|
|
585
591
|
if k <= 0:
|
|
586
592
|
raise ValueError(f"Permeability k must be positive, got {k}")
|
|
587
593
|
if h <= 0:
|
|
588
|
-
raise ValueError(f"Net pay thickness h must be positive, got {
|
|
594
|
+
raise ValueError(f"Net pay thickness h must be positive, got {_h_in} {_unit_len}")
|
|
589
595
|
if rw <= 0:
|
|
590
|
-
raise ValueError(f"Wellbore radius rw must be positive, got {
|
|
596
|
+
raise ValueError(f"Wellbore radius rw must be positive, got {_rw_in} {_unit_len}")
|
|
591
597
|
if re <= rw:
|
|
592
|
-
raise ValueError(
|
|
598
|
+
raise ValueError(
|
|
599
|
+
f"Drainage radius re ({_re_in} {_unit_len}) must be greater than "
|
|
600
|
+
f"wellbore radius rw ({_rw_in} {_unit_len})"
|
|
601
|
+
)
|
|
593
602
|
self.pr = pr # stored in psia
|
|
594
603
|
self.degf = degf # stored in degF
|
|
595
604
|
self.k = k # mD (same in both systems)
|
|
@@ -1705,6 +1714,7 @@ def fbhp(thp, completion, vlpmethod='WG', well_type='gas',
|
|
|
1705
1714
|
rsb = rsb * SM3_PER_SM3_TO_SCF_PER_STB
|
|
1706
1715
|
|
|
1707
1716
|
validate_pe_inputs(p=thp)
|
|
1717
|
+
validate_choice(well_type, ('gas', 'oil'), 'well_type')
|
|
1708
1718
|
vlpmethod = validate_methods(["vlpmethod"], [vlpmethod])
|
|
1709
1719
|
|
|
1710
1720
|
# Extract oil PVT parameters if provided (already in oilfield units from OilPVT)
|
|
@@ -1791,6 +1801,7 @@ def outflow_curve(thp, completion, vlpmethod='WG', well_type='gas',
|
|
|
1791
1801
|
'rates': list of flow rates (MMscf/d for gas, STB/d for oil; sm3/d if metric)
|
|
1792
1802
|
'bhp': list of flowing BHP values (psia; barsa if metric) at each rate
|
|
1793
1803
|
"""
|
|
1804
|
+
validate_choice(well_type, ('gas', 'oil'), 'well_type')
|
|
1794
1805
|
# Convert metric inputs to oilfield at the boundary
|
|
1795
1806
|
if metric:
|
|
1796
1807
|
thp = thp * BAR_TO_PSI
|
|
@@ -1877,6 +1888,7 @@ def ipr_curve(reservoir, well_type='gas', gas_pvt=None, oil_pvt=None,
|
|
|
1877
1888
|
gsg: Gas specific gravity. Used if gas_pvt not provided
|
|
1878
1889
|
metric: If True, inputs/outputs in Eclipse METRIC units. Default False.
|
|
1879
1890
|
"""
|
|
1891
|
+
validate_choice(well_type, ('gas', 'oil', 'water'), 'well_type')
|
|
1880
1892
|
if min_pwf is None:
|
|
1881
1893
|
min_pwf = 1.01325 if metric else 14.7
|
|
1882
1894
|
if metric:
|
|
@@ -1991,6 +2003,7 @@ def operating_point(thp, completion, reservoir,
|
|
|
1991
2003
|
vlp: VLP outflow curve dict
|
|
1992
2004
|
ipr: IPR inflow curve dict
|
|
1993
2005
|
"""
|
|
2006
|
+
validate_choice(well_type, ('gas', 'oil'), 'well_type')
|
|
1994
2007
|
# Convert metric inputs to oilfield at the boundary
|
|
1995
2008
|
if metric:
|
|
1996
2009
|
thp = thp * BAR_TO_PSI
|
|
@@ -324,6 +324,10 @@ def oil_rs(
|
|
|
324
324
|
raise ValueError(
|
|
325
325
|
"Missing one of the required inputs: sg_sp, api, rsb, for the Velarde, Blasingame & McCain Rs calculation"
|
|
326
326
|
)
|
|
327
|
+
# Degenerate: pb at (or below) atmospheric means no dissolved gas can evolve.
|
|
328
|
+
# Skip the correlation; the 0/0 in pr would otherwise return NaN silently.
|
|
329
|
+
if pb - psc <= 0:
|
|
330
|
+
return 0.0
|
|
327
331
|
xs = [_VEL_RS_A, _VEL_RS_B, _VEL_RS_C]
|
|
328
332
|
a = [
|
|
329
333
|
x[0]
|
|
@@ -75,7 +75,9 @@ def recommend_gas_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
|
|
|
75
75
|
Parameters
|
|
76
76
|
----------
|
|
77
77
|
sg : float
|
|
78
|
-
Gas specific gravity (default 0.65).
|
|
78
|
+
Gas specific gravity (default 0.65). Currently unused by the decision
|
|
79
|
+
logic — accepted for API consistency and reserved for future rules
|
|
80
|
+
(e.g. heavy gas / condensate-laden streams). Breaking removal deferred.
|
|
79
81
|
co2 : float
|
|
80
82
|
CO2 mole fraction (default 0).
|
|
81
83
|
h2s : float
|
|
@@ -90,6 +92,7 @@ def recommend_gas_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
|
|
|
90
92
|
dict of str to MethodRecommendation
|
|
91
93
|
Keys: 'zmethod', 'cmethod'.
|
|
92
94
|
"""
|
|
95
|
+
_ = sg # reserved, see docstring
|
|
93
96
|
inerts = co2 + h2s + n2 + h2
|
|
94
97
|
recs = {}
|
|
95
98
|
|
|
@@ -204,13 +207,16 @@ def recommend_vlp_method(deviation: float = 0,
|
|
|
204
207
|
deviation : float
|
|
205
208
|
Maximum wellbore deviation from vertical in degrees (default 0).
|
|
206
209
|
well_type : str
|
|
207
|
-
'gas' or 'oil' (default 'gas').
|
|
210
|
+
'gas' or 'oil' (default 'gas'). Currently unused by the decision logic
|
|
211
|
+
— accepted for API consistency and reserved for future fluid-specific
|
|
212
|
+
recommendations. Breaking removal deferred.
|
|
208
213
|
|
|
209
214
|
Returns
|
|
210
215
|
-------
|
|
211
216
|
dict of str to MethodRecommendation
|
|
212
217
|
Key: 'vlp_method'.
|
|
213
218
|
"""
|
|
219
|
+
_ = well_type # reserved, see docstring
|
|
214
220
|
recs = {}
|
|
215
221
|
|
|
216
222
|
if deviation <= 30:
|
|
@@ -39,6 +39,7 @@ __all__ = [
|
|
|
39
39
|
'SweepResult', 'TornadoEntry', 'TornadoResult',
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
+
import math
|
|
42
43
|
from dataclasses import dataclass, field
|
|
43
44
|
from typing import Callable, Dict, Any, List, Optional
|
|
44
45
|
|
|
@@ -172,11 +173,21 @@ def tornado(func: Callable, base_kwargs: Dict[str, Any],
|
|
|
172
173
|
base_raw = func(**base_kwargs)
|
|
173
174
|
base_val = _extract_result(base_raw, result_key)
|
|
174
175
|
base_result = float(base_val)
|
|
176
|
+
if not math.isfinite(base_result):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"base_result is not finite ({base_result}); "
|
|
179
|
+
f"tornado sensitivities would be undefined. "
|
|
180
|
+
f"Check base_kwargs and result_key."
|
|
181
|
+
)
|
|
175
182
|
|
|
176
183
|
entries = []
|
|
177
184
|
for param, (lo, hi) in ranges.items():
|
|
178
185
|
if param not in base_kwargs:
|
|
179
186
|
raise ValueError(f"Parameter '{param}' not found in base_kwargs")
|
|
187
|
+
if lo > hi:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"ranges['{param}'] = ({lo}, {hi}): low must be <= high"
|
|
190
|
+
)
|
|
180
191
|
|
|
181
192
|
kwargs_lo = dict(base_kwargs)
|
|
182
193
|
kwargs_lo[param] = lo
|