pyrestoolbox 3.3.0__tar.gz → 3.5.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.3.0 → pyrestoolbox-3.5.0}/PKG-INFO +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyproject.toml +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/brine/_lib_vle_engine.py +18 -8
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/brine/brine.py +49 -10
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/dca/dca.py +124 -93
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/brine.rst +13 -7
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/changelist.rst +100 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/examples.ipynb +16 -19
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/gas.rst +241 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/matbal.rst +22 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/nodal.rst +59 -11
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/oil.rst +7 -4
- pyrestoolbox-3.5.0/pyrestoolbox/gas/_hydrate.py +513 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/gas/gas.py +2030 -2280
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/matbal/matbal.py +32 -8
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/nodal/nodal.py +174 -47
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/__init__.py +1 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_compressibility.py +4 -6
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_constants.py +18 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_correlations.py +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_density.py +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_harmonize.py +3 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_pvt_class.py +17 -13
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_rate.py +4 -2
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_separator.py +10 -15
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_tables.py +24 -20
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/shared_fns/shared_fns.py +11 -8
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/simtools/simtools.py +27 -4
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/bessel.rs +7 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/lib.rs +0 -2
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/oil/density.rs +0 -46
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/oil/mod.rs +0 -31
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/spycher_pruess/mod.rs +3 -2
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/spycher_pruess/solubility.rs +801 -794
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/flash.rs +246 -427
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/mod.rs +127 -197
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/holdup_hb.rs +9 -5
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/segment_gas.rs +10 -9
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/segment_oil.rs +34 -9
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/.gitignore +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/Cargo.lock +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/Cargo.toml +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/LICENSE +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/MANIFEST.in +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/README.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/benchmark_rust_vs_python.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/build_pure_python.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/_accelerator.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/classes/classes.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/recommend.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/oil/_utils.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/recommend/recommend.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/pyrestoolbox/validate/validate.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/setup.cfg +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/setup.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/critical_properties/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/gas_viscosity/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/gwr.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/pseudopressure.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/bip.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/components.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/k_init.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/pvt_helpers.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.0}/src/vlp/static_column.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.5.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.5.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"}]
|
|
@@ -2106,15 +2106,21 @@ class SWMultiComponentFlash:
|
|
|
2106
2106
|
# specialised ks models (Dubessy CO2, Akinfiev H2S, Li HC,
|
|
2107
2107
|
# Mao-Duan N2, Duan-Sun CO2) are applied in self.calc_gamma()
|
|
2108
2108
|
# on the Python side.
|
|
2109
|
-
|
|
2109
|
+
#
|
|
2110
|
+
# Restricted to framework='proposed': the Rust flash hardcodes the
|
|
2111
|
+
# MC-3 water alpha and the proposed-framework kij_AQ. The 'dropin'
|
|
2112
|
+
# and 'sw_original' frameworks use a salinity-dependent Soreide water
|
|
2113
|
+
# alpha and different kij_AQ correlations that Rust does not implement,
|
|
2114
|
+
# so they must take the Python path to avoid a silent downgrade.
|
|
2115
|
+
if _RUST_AVAILABLE and self.framework == 'proposed':
|
|
2110
2116
|
try:
|
|
2111
2117
|
gamma_arr = np.asarray(gamma, dtype=float) if gamma is not None \
|
|
2112
2118
|
else np.ones(self.nc)
|
|
2113
|
-
V, x_r, y_r = _rust.flash_tp_rust(
|
|
2119
|
+
V, x_r, y_r, conv_r = _rust.flash_tp_rust(
|
|
2114
2120
|
T_K, P_Pa, z.tolist(), list(self.names),
|
|
2115
2121
|
0.0, mode, gamma_arr.tolist(),
|
|
2116
2122
|
)
|
|
2117
|
-
return V, np.array(x_r), np.array(y_r),
|
|
2123
|
+
return V, np.array(x_r), np.array(y_r), conv_r
|
|
2118
2124
|
except (ImportError, AttributeError):
|
|
2119
2125
|
pass
|
|
2120
2126
|
|
|
@@ -2269,8 +2275,12 @@ class SWMultiComponentFlash:
|
|
|
2269
2275
|
salinity_method = 'explicit'
|
|
2270
2276
|
|
|
2271
2277
|
# Rust acceleration path: compute gamma in Python (correct ks models),
|
|
2272
|
-
# then use Rust flash_tp for the two flashes.
|
|
2273
|
-
|
|
2278
|
+
# then use Rust flash_tp for the two flashes. Restricted to
|
|
2279
|
+
# framework='proposed' — Rust only implements the proposed-framework
|
|
2280
|
+
# water alpha (MC-3) and kij_AQ; 'dropin'/'sw_original' would be
|
|
2281
|
+
# silently downgraded to 'proposed', so they take the Python path.
|
|
2282
|
+
if (_RUST_AVAILABLE and salinity_method == 'gamma_phi'
|
|
2283
|
+
and self.framework == 'proposed'):
|
|
2274
2284
|
try:
|
|
2275
2285
|
gamma_aq = None
|
|
2276
2286
|
if self.salinity > 0:
|
|
@@ -2281,11 +2291,11 @@ class SWMultiComponentFlash:
|
|
|
2281
2291
|
else [1.0] * self.nc
|
|
2282
2292
|
names_list = list(self.names)
|
|
2283
2293
|
|
|
2284
|
-
V_aq, x_aq_r, y_aq_r = _rust.flash_tp_rust(
|
|
2294
|
+
V_aq, x_aq_r, y_aq_r, conv_aq = _rust.flash_tp_rust(
|
|
2285
2295
|
T_K, P_Pa, z.tolist(), names_list,
|
|
2286
2296
|
self.salinity, 'AQ', gamma_list,
|
|
2287
2297
|
)
|
|
2288
|
-
V_na, x_na_r, y_na_r = _rust.flash_tp_rust(
|
|
2298
|
+
V_na, x_na_r, y_na_r, conv_na = _rust.flash_tp_rust(
|
|
2289
2299
|
T_K, P_Pa, z.tolist(), names_list,
|
|
2290
2300
|
self.salinity, 'NA', [1.0] * self.nc,
|
|
2291
2301
|
)
|
|
@@ -2296,7 +2306,7 @@ class SWMultiComponentFlash:
|
|
|
2296
2306
|
result = {
|
|
2297
2307
|
'x_aq': x_aq, 'y_na': y_na, 'K_true': K_true,
|
|
2298
2308
|
'V_aq': V_aq, 'V_na': V_na,
|
|
2299
|
-
'converged_aq':
|
|
2309
|
+
'converged_aq': conv_aq, 'converged_na': conv_na,
|
|
2300
2310
|
'component_names': self.names,
|
|
2301
2311
|
'salinity_method': salinity_method,
|
|
2302
2312
|
'vlle_warning': False,
|
|
@@ -37,6 +37,8 @@ __all__ = [
|
|
|
37
37
|
'brine_props', 'CO2_Brine_Mixture', 'SoreideWhitson', 'make_pvtw_table',
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
+
import warnings
|
|
41
|
+
|
|
40
42
|
import numpy as np
|
|
41
43
|
import numpy.typing as npt
|
|
42
44
|
import pandas as pd
|
|
@@ -133,6 +135,8 @@ def brine_props(p: float = None, degf: float = None, wt: float = None, ch4_sat:
|
|
|
133
135
|
p = pres
|
|
134
136
|
if degf is None and temp is not None:
|
|
135
137
|
degf = temp
|
|
138
|
+
if wt is not None and ppm is not None:
|
|
139
|
+
raise ValueError("Supply either wt or ppm, not both.")
|
|
136
140
|
if wt is None:
|
|
137
141
|
wt = ppm / 10000 if ppm is not None else 0
|
|
138
142
|
if p is None or degf is None:
|
|
@@ -467,19 +471,21 @@ class CO2_Brine_Mixture():
|
|
|
467
471
|
>> array([0.02431245, 0.95743175])
|
|
468
472
|
|
|
469
473
|
Usage example for 175 Bara x 85 degC and 0% NaCl brine:
|
|
470
|
-
mix = brine.CO2_Brine_Mixture(pres = 175, temp = 85)
|
|
474
|
+
mix = brine.CO2_Brine_Mixture(pres = 175, temp = 85, metric = True)
|
|
471
475
|
mix.Rs # Returns sm3 dissolved CO2 / sm3 Brine
|
|
472
476
|
>> 24.742923469934272
|
|
473
477
|
|
|
474
478
|
|
|
475
479
|
"""
|
|
476
|
-
def __init__(self, pres=None, temp=None, ppm=None, metric=
|
|
480
|
+
def __init__(self, pres=None, temp=None, ppm=None, metric=False, cw_sat=False,
|
|
477
481
|
*, p=None, degf=None, wt=None):
|
|
478
482
|
# Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
|
|
479
483
|
if pres is None and p is not None:
|
|
480
484
|
pres = p
|
|
481
485
|
if temp is None and degf is not None:
|
|
482
486
|
temp = degf
|
|
487
|
+
if ppm is not None and wt is not None:
|
|
488
|
+
raise ValueError("Supply either ppm or wt, not both.")
|
|
483
489
|
if ppm is None:
|
|
484
490
|
ppm = wt * 10000 if wt is not None else 0
|
|
485
491
|
if pres is None or temp is None:
|
|
@@ -961,9 +967,10 @@ class CO2_Brine_Mixture():
|
|
|
961
967
|
"""
|
|
962
968
|
if _RUST_AVAILABLE:
|
|
963
969
|
try:
|
|
964
|
-
xco2, yco2, yh2o, rhogas, gasz = _rust.co2_brine_solubility_rust(
|
|
970
|
+
xco2, yco2, yh2o, rhogas, gasz, conv = _rust.co2_brine_solubility_rust(
|
|
965
971
|
self.pBar, self.degC, self.ppm
|
|
966
972
|
)
|
|
973
|
+
self.converged = conv
|
|
967
974
|
self.x = np.array([xco2, 1.0 - xco2 - (self.xSalt if self.xSalt else 0.0)])
|
|
968
975
|
self.y = np.array([yco2, yh2o])
|
|
969
976
|
self.rhoGas = rhogas
|
|
@@ -1436,8 +1443,15 @@ class SoreideWhitson:
|
|
|
1436
1443
|
y_N2: Mole fraction N2 in dry gas (default 0)
|
|
1437
1444
|
y_H2: Mole fraction H2 in dry gas (default 0)
|
|
1438
1445
|
sg: Gas specific gravity — used to estimate HC split among C1-C4 (default 0.65)
|
|
1439
|
-
metric: Boolean for units (True=metric, False=oilfield). Default
|
|
1446
|
+
metric: Boolean for units (True=metric, False=oilfield). Default False.
|
|
1440
1447
|
cw_sat: If True, also calculate saturated compressibility (default False)
|
|
1448
|
+
framework: VLE framework. 'proposed' (default, Soreide-Whitson 1992 re-fit),
|
|
1449
|
+
'sw_original' (original 1992 published), or 'dropin' (fitted to PR-EOS with
|
|
1450
|
+
brine-aware water alpha). Affects kij and ks correlations.
|
|
1451
|
+
salinity_method: How salinity enters the flash. 'gamma_phi' (default, Sechenov
|
|
1452
|
+
salting-out via activity coefficient), 'embedded' (salinity inside kij —
|
|
1453
|
+
only for 'dropin'/'sw_original'), 'explicit' (brine treated as a component),
|
|
1454
|
+
'sechenov' (legacy alias for gamma_phi), or 'auto' (pick per-gas defaults).
|
|
1441
1455
|
|
|
1442
1456
|
Returns object with following calculated properties:
|
|
1443
1457
|
.x : Dict of dissolved gas mole fractions, e.g. {'CO2': 0.024, 'CH4': 0.0015}
|
|
@@ -1462,7 +1476,7 @@ class SoreideWhitson:
|
|
|
1462
1476
|
mix.Rs # Returns per-gas Rs dict, e.g. {'CO2': 15.2}
|
|
1463
1477
|
|
|
1464
1478
|
# Mixed gas, metric units
|
|
1465
|
-
mix = brine.SoreideWhitson(pres=200, temp=80, ppm=10000, y_CO2=0.1, y_H2S=0.05, sg=0.7)
|
|
1479
|
+
mix = brine.SoreideWhitson(pres=200, temp=80, ppm=10000, y_CO2=0.1, y_H2S=0.05, sg=0.7, metric=True)
|
|
1466
1480
|
mix.bDen # Returns [gas-saturated, gas-free, freshwater] densities
|
|
1467
1481
|
|
|
1468
1482
|
References:
|
|
@@ -1482,13 +1496,20 @@ class SoreideWhitson:
|
|
|
1482
1496
|
Murphy, W.R. and Gaines, T.M. (1974), J. Chem. Eng. Data 19(4), 359-362.
|
|
1483
1497
|
"""
|
|
1484
1498
|
|
|
1499
|
+
_VALID_FRAMEWORKS = ('proposed', 'sw_original', 'dropin')
|
|
1500
|
+
_VALID_SALINITY_METHODS = ('gamma_phi', 'embedded', 'explicit', 'auto', 'sechenov')
|
|
1501
|
+
|
|
1485
1502
|
def __init__(self, pres=None, temp=None, ppm=None, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0,
|
|
1486
|
-
sg=0.65, metric=
|
|
1503
|
+
sg=0.65, metric=False, cw_sat=False,
|
|
1504
|
+
framework='proposed', salinity_method='gamma_phi',
|
|
1505
|
+
*, p=None, degf=None, wt=None):
|
|
1487
1506
|
# Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
|
|
1488
1507
|
if pres is None and p is not None:
|
|
1489
1508
|
pres = p
|
|
1490
1509
|
if temp is None and degf is not None:
|
|
1491
1510
|
temp = degf
|
|
1511
|
+
if ppm is not None and wt is not None:
|
|
1512
|
+
raise ValueError("Supply either ppm or wt, not both.")
|
|
1492
1513
|
if ppm is None:
|
|
1493
1514
|
ppm = wt * 10000 if wt is not None else 0
|
|
1494
1515
|
if pres is None or temp is None:
|
|
@@ -1497,6 +1518,22 @@ class SoreideWhitson:
|
|
|
1497
1518
|
raise ValueError(f"ppm must be non-negative, got {ppm}")
|
|
1498
1519
|
if ppm >= 1e6:
|
|
1499
1520
|
raise ValueError(f"ppm must be less than 1,000,000, got {ppm}")
|
|
1521
|
+
if framework not in self._VALID_FRAMEWORKS:
|
|
1522
|
+
raise ValueError(
|
|
1523
|
+
f"Invalid framework: {framework!r}. Valid options: {list(self._VALID_FRAMEWORKS)}"
|
|
1524
|
+
)
|
|
1525
|
+
if salinity_method not in self._VALID_SALINITY_METHODS:
|
|
1526
|
+
raise ValueError(
|
|
1527
|
+
f"Invalid salinity_method: {salinity_method!r}. Valid options: {list(self._VALID_SALINITY_METHODS)}"
|
|
1528
|
+
)
|
|
1529
|
+
if framework == 'proposed' and salinity_method == 'embedded':
|
|
1530
|
+
warnings.warn(
|
|
1531
|
+
"framework='proposed' does not define embedded-salinity kij; engine will "
|
|
1532
|
+
"fall back to the 'gamma_phi' behaviour.",
|
|
1533
|
+
stacklevel=2,
|
|
1534
|
+
)
|
|
1535
|
+
self.framework = framework
|
|
1536
|
+
self.salinity_method = salinity_method
|
|
1500
1537
|
# Validate pressure and temperature (convert to oilfield units for validation)
|
|
1501
1538
|
_p_val = pres if not metric else pres * BAR2PSI
|
|
1502
1539
|
_t_val = temp if not metric else temp * 1.8 + 32
|
|
@@ -1742,8 +1779,8 @@ class SoreideWhitson:
|
|
|
1742
1779
|
y_H2S=self.gas_comp.get('H2S', 0),
|
|
1743
1780
|
y_H2=self.gas_comp.get('H2', 0),
|
|
1744
1781
|
method='flash',
|
|
1745
|
-
salinity_method=
|
|
1746
|
-
framework=
|
|
1782
|
+
salinity_method=self.salinity_method,
|
|
1783
|
+
framework=self.framework,
|
|
1747
1784
|
)
|
|
1748
1785
|
|
|
1749
1786
|
self.x = x_gas
|
|
@@ -1903,8 +1940,10 @@ class SoreideWhitson:
|
|
|
1903
1940
|
gas_ply = _VLE_TO_PLYASUNOV.get(gas_vle, gas_vle.upper())
|
|
1904
1941
|
vphi_eff_p1 += yi * _plyasunov_V_phi(gas_ply, tKel, Mpa_p1)
|
|
1905
1942
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1943
|
+
# Garcia Eq. 18 at P+1, same algebraically reformulated (non-singular)
|
|
1944
|
+
# form as Step 3 — multiply top/bot through by M1*x1.
|
|
1945
|
+
numerator_p1 = MWWAT * x1 + self.x_total * mw_eff
|
|
1946
|
+
denom_p1 = self.x_total * vphi_eff_p1 + MWWAT * x1 / rho_brine_p1_gcc
|
|
1908
1947
|
rho_p1_gcc = numerator_p1 / denom_p1
|
|
1909
1948
|
else:
|
|
1910
1949
|
rho_p1_gcc = rho_brine_p1_gcc
|
|
@@ -51,7 +51,8 @@ __all__ = [
|
|
|
51
51
|
|
|
52
52
|
import numpy as np
|
|
53
53
|
from dataclasses import dataclass, field
|
|
54
|
-
from typing import Optional
|
|
54
|
+
from typing import Optional, Union
|
|
55
|
+
from numpy.typing import ArrayLike
|
|
55
56
|
|
|
56
57
|
from pyrestoolbox.shared_fns import convert_to_numpy, process_output, ransac_linreg
|
|
57
58
|
from pyrestoolbox._accelerator import RUST_AVAILABLE as _RUST_AVAILABLE
|
|
@@ -59,6 +60,66 @@ if _RUST_AVAILABLE:
|
|
|
59
60
|
from pyrestoolbox import _native as _rust
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Named constants — extracted from inline magic numbers per CLAUDE.md rules.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# Arps hyperbolic b-grid search (Arps 1945; b physically in (0, 1))
|
|
68
|
+
# 0.05-0.95 avoids exponential (b=0) / harmonic (b=1) degeneracies.
|
|
69
|
+
_B_GRID_MIN = 0.05
|
|
70
|
+
_B_GRID_MAX = 0.96
|
|
71
|
+
_B_GRID_STEP = 0.01
|
|
72
|
+
|
|
73
|
+
# Duong (2011) cumulative integration grid
|
|
74
|
+
_DUONG_TRAP_LB = 0.001 # Lower trap bound to avoid t=0 singularity
|
|
75
|
+
_DUONG_GRID_MIN = 500 # Minimum trap grid points per integration
|
|
76
|
+
_DUONG_GRID_DENSITY = 10 # Additional points per unit t
|
|
77
|
+
|
|
78
|
+
# Duong curve_fit bounds and initial guess (Duong 2011, Eq. 5)
|
|
79
|
+
_DUONG_BOUNDS_LO = (0.0, 0.01, 1.001)
|
|
80
|
+
_DUONG_BOUNDS_HI_QI_FACTOR = 5.0 # Upper qi bound = first q * factor
|
|
81
|
+
_DUONG_BOUNDS_HI_A = 10.0
|
|
82
|
+
_DUONG_BOUNDS_HI_M = 3.0
|
|
83
|
+
_DUONG_P0_A = 1.0 # Initial guess a
|
|
84
|
+
_DUONG_P0_M = 1.2 # Initial guess m
|
|
85
|
+
|
|
86
|
+
# scipy.curve_fit iteration cap (covers Duong + logistic fits)
|
|
87
|
+
_CURVE_FIT_MAXFEV = 5000
|
|
88
|
+
|
|
89
|
+
# Numerical floors / near-zero guards
|
|
90
|
+
_HYPER_INNER_FLOOR = 1e-10 # Floor for (1 - di*Np*exp/qi)^(1/exp) argument
|
|
91
|
+
_ZERO_DIV_EPS = 1e-30 # General division-by-zero guard
|
|
92
|
+
|
|
93
|
+
# Logistic ratio curve_fit (unconventional GOR/WOR trending)
|
|
94
|
+
_LOGISTIC_P0_RMAX_INIT = 1.5 # Initial Rmax guess = max(ratio) * factor
|
|
95
|
+
_LOGISTIC_P0_TC = 0.01 # Initial carryover constant
|
|
96
|
+
_LOGISTIC_P0_ALPHA = 10.0 # Initial curvature
|
|
97
|
+
_LOGISTIC_BOUNDS_LO = (1e-8, 1e-3) # Lower (tc, alpha) bounds
|
|
98
|
+
_LOGISTIC_BOUNDS_RMAX_FACTOR = 5.0 # Upper Rmax = max(ratio) * factor
|
|
99
|
+
_LOGISTIC_BOUNDS_HI = 1e6 # Upper tc / alpha bound
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_decline_result(method, q_obs, q_pred, **params):
|
|
103
|
+
"""DRY helper: compute R-squared + residuals, return a DeclineResult.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
method : str
|
|
108
|
+
q_obs, q_pred : np.ndarray (observed and predicted rates)
|
|
109
|
+
**params : scalar DeclineResult fields (qi, di, b, a, m)
|
|
110
|
+
"""
|
|
111
|
+
residuals = q_obs - q_pred
|
|
112
|
+
ss_res = np.sum(residuals ** 2)
|
|
113
|
+
ss_tot = np.sum((q_obs - np.mean(q_obs)) ** 2)
|
|
114
|
+
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
115
|
+
return DeclineResult(
|
|
116
|
+
method=method,
|
|
117
|
+
r_squared=r2,
|
|
118
|
+
residuals=residuals,
|
|
119
|
+
**params,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
62
123
|
def _fmt_array(a, name):
|
|
63
124
|
if a is None:
|
|
64
125
|
return f"{name}=None"
|
|
@@ -189,7 +250,7 @@ class RatioResult:
|
|
|
189
250
|
)
|
|
190
251
|
|
|
191
252
|
|
|
192
|
-
def arps_rate(qi, di, b, t):
|
|
253
|
+
def arps_rate(qi: float, di: float, b: float, t: ArrayLike) -> Union[float, np.ndarray]:
|
|
193
254
|
"""Arps decline rate.
|
|
194
255
|
|
|
195
256
|
Parameters
|
|
@@ -226,7 +287,7 @@ def arps_rate(qi, di, b, t):
|
|
|
226
287
|
return process_output(q, is_list)
|
|
227
288
|
|
|
228
289
|
|
|
229
|
-
def arps_cum(qi, di, b, t):
|
|
290
|
+
def arps_cum(qi: float, di: float, b: float, t: ArrayLike) -> Union[float, np.ndarray]:
|
|
230
291
|
"""Arps cumulative production.
|
|
231
292
|
|
|
232
293
|
Parameters
|
|
@@ -263,7 +324,7 @@ def arps_cum(qi, di, b, t):
|
|
|
263
324
|
return process_output(Qcum, is_list)
|
|
264
325
|
|
|
265
326
|
|
|
266
|
-
def duong_rate(qi, a, m, t):
|
|
327
|
+
def duong_rate(qi: float, a: float, m: float, t: ArrayLike) -> Union[float, np.ndarray]:
|
|
267
328
|
"""Duong decline rate for unconventional reservoirs.
|
|
268
329
|
|
|
269
330
|
q(t) = qi * t^(-m) * exp(a/(1-m) * (t^(1-m) - 1))
|
|
@@ -296,7 +357,7 @@ def duong_rate(qi, a, m, t):
|
|
|
296
357
|
return process_output(q, is_list)
|
|
297
358
|
|
|
298
359
|
|
|
299
|
-
def duong_cum(qi, a, m, t):
|
|
360
|
+
def duong_cum(qi: float, a: float, m: float, t: ArrayLike) -> Union[float, np.ndarray]:
|
|
300
361
|
"""Duong cumulative production via trapezoidal integration.
|
|
301
362
|
|
|
302
363
|
Parameters
|
|
@@ -328,15 +389,15 @@ def duong_cum(qi, a, m, t):
|
|
|
328
389
|
# integrates over a descending axis and returns negative cumulative.
|
|
329
390
|
results = np.zeros_like(t, dtype=float)
|
|
330
391
|
for i, ti in enumerate(t):
|
|
331
|
-
lower = min(
|
|
332
|
-
t_fine = np.linspace(lower, ti, max(
|
|
392
|
+
lower = min(_DUONG_TRAP_LB, ti * _DUONG_TRAP_LB)
|
|
393
|
+
t_fine = np.linspace(lower, ti, max(_DUONG_GRID_MIN, int(ti * _DUONG_GRID_DENSITY)))
|
|
333
394
|
q_fine = qi * t_fine ** (-m) * np.exp(a / (1.0 - m) * (t_fine ** (1.0 - m) - 1.0))
|
|
334
395
|
results[i] = np.trapezoid(q_fine, t_fine)
|
|
335
396
|
|
|
336
397
|
return process_output(results, is_list)
|
|
337
398
|
|
|
338
399
|
|
|
339
|
-
def eur(qi, di, b, q_min):
|
|
400
|
+
def eur(qi: float, di: float, b: float, q_min: float) -> float:
|
|
340
401
|
"""Estimated ultimate recovery for Arps decline.
|
|
341
402
|
|
|
342
403
|
Parameters
|
|
@@ -382,13 +443,7 @@ def _fit_exponential(t, q):
|
|
|
382
443
|
if di <= 0:
|
|
383
444
|
return None
|
|
384
445
|
q_pred = qi * np.exp(-di * t)
|
|
385
|
-
|
|
386
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
387
|
-
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
388
|
-
return DeclineResult(
|
|
389
|
-
method='exponential', qi=qi, di=di, b=0.0,
|
|
390
|
-
r_squared=r2, residuals=q - q_pred,
|
|
391
|
-
)
|
|
446
|
+
return _build_decline_result('exponential', q, q_pred, qi=qi, di=di, b=0.0)
|
|
392
447
|
|
|
393
448
|
|
|
394
449
|
def _fit_harmonic(t, q):
|
|
@@ -403,13 +458,7 @@ def _fit_harmonic(t, q):
|
|
|
403
458
|
if di <= 0:
|
|
404
459
|
return None
|
|
405
460
|
q_pred = qi / (1.0 + di * t)
|
|
406
|
-
|
|
407
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
408
|
-
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
409
|
-
return DeclineResult(
|
|
410
|
-
method='harmonic', qi=qi, di=di, b=1.0,
|
|
411
|
-
r_squared=r2, residuals=q - q_pred,
|
|
412
|
-
)
|
|
461
|
+
return _build_decline_result('harmonic', q, q_pred, qi=qi, di=di, b=1.0)
|
|
413
462
|
|
|
414
463
|
|
|
415
464
|
def _fit_hyperbolic(t, q):
|
|
@@ -423,18 +472,14 @@ def _fit_hyperbolic(t, q):
|
|
|
423
472
|
try:
|
|
424
473
|
qi, di, b, r2 = _rust.fit_hyperbolic_rust(t.tolist(), q.tolist())
|
|
425
474
|
q_pred = qi / (1.0 + b * di * t) ** (1.0 / b)
|
|
426
|
-
return
|
|
427
|
-
method='hyperbolic', qi=qi, di=di, b=b,
|
|
428
|
-
r_squared=r2, residuals=q - q_pred,
|
|
429
|
-
)
|
|
475
|
+
return _build_decline_result('hyperbolic', q, q_pred, qi=qi, di=di, b=b)
|
|
430
476
|
except (ImportError, AttributeError):
|
|
431
477
|
pass
|
|
432
478
|
|
|
433
479
|
best_r2 = -np.inf
|
|
434
480
|
best_result = None
|
|
435
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
436
481
|
|
|
437
|
-
for b_trial in np.arange(
|
|
482
|
+
for b_trial in np.arange(_B_GRID_MIN, _B_GRID_MAX, _B_GRID_STEP):
|
|
438
483
|
# Transform: Y = q^(-b) is linear in t
|
|
439
484
|
Y = q ** (-b_trial)
|
|
440
485
|
slope, intercept, _ = ransac_linreg(t, Y)
|
|
@@ -451,15 +496,10 @@ def _fit_hyperbolic(t, q):
|
|
|
451
496
|
|
|
452
497
|
# Compute R-squared in original rate space
|
|
453
498
|
q_pred = qi / (1.0 + b_trial * di * t) ** (1.0 / b_trial)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
best_r2 = r2
|
|
459
|
-
best_result = DeclineResult(
|
|
460
|
-
method='hyperbolic', qi=qi, di=di, b=b_trial,
|
|
461
|
-
r_squared=r2, residuals=q - q_pred,
|
|
462
|
-
)
|
|
499
|
+
result = _build_decline_result('hyperbolic', q, q_pred, qi=qi, di=di, b=b_trial)
|
|
500
|
+
if result.r_squared > best_r2:
|
|
501
|
+
best_r2 = result.r_squared
|
|
502
|
+
best_result = result
|
|
463
503
|
|
|
464
504
|
return best_result
|
|
465
505
|
|
|
@@ -478,20 +518,19 @@ def _fit_duong(t, q):
|
|
|
478
518
|
t_f, q_f = t[mask], q[mask]
|
|
479
519
|
|
|
480
520
|
try:
|
|
481
|
-
popt, _ = curve_fit(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
491
|
-
return DeclineResult(
|
|
492
|
-
method='duong', qi=qi, a=a, m=m,
|
|
493
|
-
r_squared=r2, residuals=q_f - q_pred_valid,
|
|
521
|
+
popt, _ = curve_fit(
|
|
522
|
+
duong_func, t_f, q_f,
|
|
523
|
+
p0=[q_f[0], _DUONG_P0_A, _DUONG_P0_M],
|
|
524
|
+
bounds=(
|
|
525
|
+
list(_DUONG_BOUNDS_LO),
|
|
526
|
+
[q_f[0] * _DUONG_BOUNDS_HI_QI_FACTOR,
|
|
527
|
+
_DUONG_BOUNDS_HI_A, _DUONG_BOUNDS_HI_M],
|
|
528
|
+
),
|
|
529
|
+
maxfev=_CURVE_FIT_MAXFEV,
|
|
494
530
|
)
|
|
531
|
+
qi, a, m = popt
|
|
532
|
+
q_pred_valid = duong_func(t_f, qi, a, m)
|
|
533
|
+
return _build_decline_result('duong', q_f, q_pred_valid, qi=qi, a=a, m=m)
|
|
495
534
|
except (RuntimeError, ValueError):
|
|
496
535
|
return None
|
|
497
536
|
|
|
@@ -507,13 +546,7 @@ def _fit_exponential_cum(Np, q):
|
|
|
507
546
|
if di <= 0 or qi <= 0:
|
|
508
547
|
return None
|
|
509
548
|
q_pred = qi - di * Np
|
|
510
|
-
|
|
511
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
512
|
-
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
513
|
-
return DeclineResult(
|
|
514
|
-
method='exponential', qi=qi, di=di, b=0.0,
|
|
515
|
-
r_squared=r2, residuals=q - q_pred,
|
|
516
|
-
)
|
|
549
|
+
return _build_decline_result('exponential', q, q_pred, qi=qi, di=di, b=0.0)
|
|
517
550
|
|
|
518
551
|
|
|
519
552
|
def _fit_harmonic_cum(Np, q):
|
|
@@ -529,13 +562,7 @@ def _fit_harmonic_cum(Np, q):
|
|
|
529
562
|
if di <= 0 or qi <= 0:
|
|
530
563
|
return None
|
|
531
564
|
q_pred = qi * np.exp(-di / qi * Np)
|
|
532
|
-
|
|
533
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
534
|
-
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
|
|
535
|
-
return DeclineResult(
|
|
536
|
-
method='harmonic', qi=qi, di=di, b=1.0,
|
|
537
|
-
r_squared=r2, residuals=q - q_pred,
|
|
538
|
-
)
|
|
565
|
+
return _build_decline_result('harmonic', q, q_pred, qi=qi, di=di, b=1.0)
|
|
539
566
|
|
|
540
567
|
|
|
541
568
|
def _fit_hyperbolic_cum(Np, q):
|
|
@@ -550,20 +577,16 @@ def _fit_hyperbolic_cum(Np, q):
|
|
|
550
577
|
try:
|
|
551
578
|
qi, di, b, r2 = _rust.fit_hyperbolic_cum_rust(Np.tolist(), q.tolist())
|
|
552
579
|
exp = 1.0 - b
|
|
553
|
-
inner = np.maximum(1.0 - exp * di * Np / qi,
|
|
580
|
+
inner = np.maximum(1.0 - exp * di * Np / qi, _HYPER_INNER_FLOOR)
|
|
554
581
|
q_pred = qi * inner ** (1.0 / exp)
|
|
555
|
-
return
|
|
556
|
-
method='hyperbolic', qi=qi, di=di, b=b,
|
|
557
|
-
r_squared=r2, residuals=q - q_pred,
|
|
558
|
-
)
|
|
582
|
+
return _build_decline_result('hyperbolic', q, q_pred, qi=qi, di=di, b=b)
|
|
559
583
|
except (ImportError, AttributeError):
|
|
560
584
|
pass
|
|
561
585
|
|
|
562
586
|
best_r2 = -np.inf
|
|
563
587
|
best_result = None
|
|
564
|
-
ss_tot = np.sum((q - np.mean(q)) ** 2)
|
|
565
588
|
|
|
566
|
-
for b_trial in np.arange(
|
|
589
|
+
for b_trial in np.arange(_B_GRID_MIN, _B_GRID_MAX, _B_GRID_STEP):
|
|
567
590
|
exp = 1.0 - b_trial
|
|
568
591
|
# Transform: Np = A + B * q^(1-b)
|
|
569
592
|
X = q ** exp
|
|
@@ -572,7 +595,7 @@ def _fit_hyperbolic_cum(Np, q):
|
|
|
572
595
|
# A = intercept = qi / ((1-b)*di)
|
|
573
596
|
# B = slope = -qi^b / ((1-b)*di)
|
|
574
597
|
# B/A = -qi^b / qi = -qi^(b-1) = -1/qi^(1-b)
|
|
575
|
-
if abs(slope) <
|
|
598
|
+
if abs(slope) < _ZERO_DIV_EPS or abs(intercept) < _ZERO_DIV_EPS:
|
|
576
599
|
continue
|
|
577
600
|
ratio = slope / intercept # = -1/qi^(1-b)
|
|
578
601
|
if ratio >= 0:
|
|
@@ -588,23 +611,20 @@ def _fit_hyperbolic_cum(Np, q):
|
|
|
588
611
|
continue
|
|
589
612
|
|
|
590
613
|
# Compute R-squared in original rate space
|
|
591
|
-
inner = 1.0 - exp * di * Np / qi
|
|
592
|
-
inner = np.maximum(inner, 1e-10)
|
|
614
|
+
inner = np.maximum(1.0 - exp * di * Np / qi, _HYPER_INNER_FLOOR)
|
|
593
615
|
q_pred = qi * inner ** (1.0 / exp)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
best_r2 = r2
|
|
599
|
-
best_result = DeclineResult(
|
|
600
|
-
method='hyperbolic', qi=qi, di=di, b=b_trial,
|
|
601
|
-
r_squared=r2, residuals=q - q_pred,
|
|
602
|
-
)
|
|
616
|
+
result = _build_decline_result('hyperbolic', q, q_pred, qi=qi, di=di, b=b_trial)
|
|
617
|
+
if result.r_squared > best_r2:
|
|
618
|
+
best_r2 = result.r_squared
|
|
619
|
+
best_result = result
|
|
603
620
|
|
|
604
621
|
return best_result
|
|
605
622
|
|
|
606
623
|
|
|
607
|
-
def fit_decline_cum(Np, q, method='best',
|
|
624
|
+
def fit_decline_cum(Np: ArrayLike, q: ArrayLike, method: str = 'best',
|
|
625
|
+
t_calendar: Optional[ArrayLike] = None,
|
|
626
|
+
Np_start: Optional[float] = None,
|
|
627
|
+
Np_end: Optional[float] = None) -> 'DeclineResult':
|
|
608
628
|
"""Fit a decline model to rate-vs-cumulative data.
|
|
609
629
|
|
|
610
630
|
Eliminates time from the Arps equations to fit q as a function of Np.
|
|
@@ -708,7 +728,9 @@ def fit_decline_cum(Np, q, method='best', t_calendar=None, Np_start=None, Np_end
|
|
|
708
728
|
return best
|
|
709
729
|
|
|
710
730
|
|
|
711
|
-
def fit_decline(t, q, method='best',
|
|
731
|
+
def fit_decline(t: ArrayLike, q: ArrayLike, method: str = 'best',
|
|
732
|
+
t_start: Optional[float] = None,
|
|
733
|
+
t_end: Optional[float] = None) -> 'DeclineResult':
|
|
712
734
|
"""Fit a decline model to production data.
|
|
713
735
|
|
|
714
736
|
Parameters
|
|
@@ -832,11 +854,17 @@ def _fit_ratio_logistic(x, ratio):
|
|
|
832
854
|
return Rmax / (1.0 + c * np.exp(-b * x))
|
|
833
855
|
|
|
834
856
|
try:
|
|
835
|
-
Rmax_guess = np.max(ratio) *
|
|
836
|
-
popt, _ = curve_fit(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
857
|
+
Rmax_guess = np.max(ratio) * _LOGISTIC_P0_RMAX_INIT
|
|
858
|
+
popt, _ = curve_fit(
|
|
859
|
+
logistic_func, x, ratio,
|
|
860
|
+
p0=[Rmax_guess, _LOGISTIC_P0_TC, _LOGISTIC_P0_ALPHA],
|
|
861
|
+
bounds=(
|
|
862
|
+
[0.0, _LOGISTIC_BOUNDS_LO[0], _LOGISTIC_BOUNDS_LO[1]],
|
|
863
|
+
[Rmax_guess * _LOGISTIC_BOUNDS_RMAX_FACTOR,
|
|
864
|
+
_LOGISTIC_BOUNDS_HI, _LOGISTIC_BOUNDS_HI],
|
|
865
|
+
),
|
|
866
|
+
maxfev=_CURVE_FIT_MAXFEV,
|
|
867
|
+
)
|
|
840
868
|
Rmax, b, c = popt
|
|
841
869
|
r_pred = logistic_func(x, Rmax, b, c)
|
|
842
870
|
ss_res = np.sum((ratio - r_pred) ** 2)
|
|
@@ -850,7 +878,8 @@ def _fit_ratio_logistic(x, ratio):
|
|
|
850
878
|
return None
|
|
851
879
|
|
|
852
880
|
|
|
853
|
-
def fit_ratio(x, ratio
|
|
881
|
+
def fit_ratio(x: ArrayLike, ratio: ArrayLike,
|
|
882
|
+
method: str = 'best', domain: str = 'cum') -> 'RatioResult':
|
|
854
883
|
"""Fit a ratio model (e.g. GOR, WOR) to data.
|
|
855
884
|
|
|
856
885
|
Parameters
|
|
@@ -904,7 +933,7 @@ def fit_ratio(x, ratio, method='best', domain='cum'):
|
|
|
904
933
|
return result
|
|
905
934
|
|
|
906
935
|
|
|
907
|
-
def ratio_forecast(result, x):
|
|
936
|
+
def ratio_forecast(result: 'RatioResult', x: ArrayLike) -> np.ndarray:
|
|
908
937
|
"""Evaluate a fitted ratio model at given x values.
|
|
909
938
|
|
|
910
939
|
Parameters
|
|
@@ -935,7 +964,9 @@ def ratio_forecast(result, x):
|
|
|
935
964
|
return process_output(r, is_list)
|
|
936
965
|
|
|
937
966
|
|
|
938
|
-
def forecast(result, t_end, dt
|
|
967
|
+
def forecast(result: 'DeclineResult', t_end: float, dt: float = 1.0,
|
|
968
|
+
q_min: float = 0.0, uptime: float = 1.0,
|
|
969
|
+
ratios: Optional[dict] = None) -> 'ForecastResult':
|
|
939
970
|
"""Generate a rate and cumulative forecast from a fitted decline model.
|
|
940
971
|
|
|
941
972
|
Parameters
|