pyrestoolbox 3.3.0__tar.gz → 3.4.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.4.0}/PKG-INFO +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyproject.toml +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/brine.py +43 -7
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/dca/dca.py +124 -93
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/brine.rst +13 -7
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/changelist.rst +61 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/examples.ipynb +16 -19
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal.rst +55 -7
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/oil.rst +7 -4
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/nodal/nodal.py +170 -47
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/__init__.py +1 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_compressibility.py +4 -6
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_constants.py +18 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_correlations.py +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_density.py +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_harmonize.py +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_pvt_class.py +17 -13
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_rate.py +4 -2
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_separator.py +10 -15
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_tables.py +1 -3
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/simtools/simtools.py +4 -4
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_hb.rs +9 -5
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/segment_gas.rs +1 -1
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/segment_oil.rs +30 -5
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/.gitignore +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/Cargo.lock +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/Cargo.toml +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/LICENSE +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/MANIFEST.in +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/README.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/benchmark_rust_vs_python.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/build_pure_python.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/_accelerator.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/_lib_vle_engine.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/classes/classes.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/gas.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/matbal.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/recommend.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/gas/gas.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/matbal/matbal.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_utils.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/recommend/recommend.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/pyrestoolbox/validate/validate.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/setup.cfg +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/setup.py +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/bessel.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/critical_properties/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/gas_viscosity/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/gwr.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/lib.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/oil/density.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/oil/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/pseudopressure.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/spycher_pruess/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/spycher_pruess/solubility.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/bip.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/components.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/flash.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/k_init.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/pvt_helpers.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.0}/src/vlp/static_column.rs +0 -0
- {pyrestoolbox-3.3.0 → pyrestoolbox-3.4.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.4.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"}]
|
|
@@ -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:
|
|
@@ -1436,8 +1442,15 @@ class SoreideWhitson:
|
|
|
1436
1442
|
y_N2: Mole fraction N2 in dry gas (default 0)
|
|
1437
1443
|
y_H2: Mole fraction H2 in dry gas (default 0)
|
|
1438
1444
|
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
|
|
1445
|
+
metric: Boolean for units (True=metric, False=oilfield). Default False.
|
|
1440
1446
|
cw_sat: If True, also calculate saturated compressibility (default False)
|
|
1447
|
+
framework: VLE framework. 'proposed' (default, Soreide-Whitson 1992 re-fit),
|
|
1448
|
+
'sw_original' (original 1992 published), or 'dropin' (fitted to PR-EOS with
|
|
1449
|
+
brine-aware water alpha). Affects kij and ks correlations.
|
|
1450
|
+
salinity_method: How salinity enters the flash. 'gamma_phi' (default, Sechenov
|
|
1451
|
+
salting-out via activity coefficient), 'embedded' (salinity inside kij —
|
|
1452
|
+
only for 'dropin'/'sw_original'), 'explicit' (brine treated as a component),
|
|
1453
|
+
'sechenov' (legacy alias for gamma_phi), or 'auto' (pick per-gas defaults).
|
|
1441
1454
|
|
|
1442
1455
|
Returns object with following calculated properties:
|
|
1443
1456
|
.x : Dict of dissolved gas mole fractions, e.g. {'CO2': 0.024, 'CH4': 0.0015}
|
|
@@ -1462,7 +1475,7 @@ class SoreideWhitson:
|
|
|
1462
1475
|
mix.Rs # Returns per-gas Rs dict, e.g. {'CO2': 15.2}
|
|
1463
1476
|
|
|
1464
1477
|
# 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)
|
|
1478
|
+
mix = brine.SoreideWhitson(pres=200, temp=80, ppm=10000, y_CO2=0.1, y_H2S=0.05, sg=0.7, metric=True)
|
|
1466
1479
|
mix.bDen # Returns [gas-saturated, gas-free, freshwater] densities
|
|
1467
1480
|
|
|
1468
1481
|
References:
|
|
@@ -1482,13 +1495,20 @@ class SoreideWhitson:
|
|
|
1482
1495
|
Murphy, W.R. and Gaines, T.M. (1974), J. Chem. Eng. Data 19(4), 359-362.
|
|
1483
1496
|
"""
|
|
1484
1497
|
|
|
1498
|
+
_VALID_FRAMEWORKS = ('proposed', 'sw_original', 'dropin')
|
|
1499
|
+
_VALID_SALINITY_METHODS = ('gamma_phi', 'embedded', 'explicit', 'auto', 'sechenov')
|
|
1500
|
+
|
|
1485
1501
|
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=
|
|
1502
|
+
sg=0.65, metric=False, cw_sat=False,
|
|
1503
|
+
framework='proposed', salinity_method='gamma_phi',
|
|
1504
|
+
*, p=None, degf=None, wt=None):
|
|
1487
1505
|
# Resolve parameter aliases (p/degf/wt -> pres/temp/ppm)
|
|
1488
1506
|
if pres is None and p is not None:
|
|
1489
1507
|
pres = p
|
|
1490
1508
|
if temp is None and degf is not None:
|
|
1491
1509
|
temp = degf
|
|
1510
|
+
if ppm is not None and wt is not None:
|
|
1511
|
+
raise ValueError("Supply either ppm or wt, not both.")
|
|
1492
1512
|
if ppm is None:
|
|
1493
1513
|
ppm = wt * 10000 if wt is not None else 0
|
|
1494
1514
|
if pres is None or temp is None:
|
|
@@ -1497,6 +1517,22 @@ class SoreideWhitson:
|
|
|
1497
1517
|
raise ValueError(f"ppm must be non-negative, got {ppm}")
|
|
1498
1518
|
if ppm >= 1e6:
|
|
1499
1519
|
raise ValueError(f"ppm must be less than 1,000,000, got {ppm}")
|
|
1520
|
+
if framework not in self._VALID_FRAMEWORKS:
|
|
1521
|
+
raise ValueError(
|
|
1522
|
+
f"Invalid framework: {framework!r}. Valid options: {list(self._VALID_FRAMEWORKS)}"
|
|
1523
|
+
)
|
|
1524
|
+
if salinity_method not in self._VALID_SALINITY_METHODS:
|
|
1525
|
+
raise ValueError(
|
|
1526
|
+
f"Invalid salinity_method: {salinity_method!r}. Valid options: {list(self._VALID_SALINITY_METHODS)}"
|
|
1527
|
+
)
|
|
1528
|
+
if framework == 'proposed' and salinity_method == 'embedded':
|
|
1529
|
+
warnings.warn(
|
|
1530
|
+
"framework='proposed' does not define embedded-salinity kij; engine will "
|
|
1531
|
+
"fall back to the 'gamma_phi' behaviour.",
|
|
1532
|
+
stacklevel=2,
|
|
1533
|
+
)
|
|
1534
|
+
self.framework = framework
|
|
1535
|
+
self.salinity_method = salinity_method
|
|
1500
1536
|
# Validate pressure and temperature (convert to oilfield units for validation)
|
|
1501
1537
|
_p_val = pres if not metric else pres * BAR2PSI
|
|
1502
1538
|
_t_val = temp if not metric else temp * 1.8 + 32
|
|
@@ -1742,8 +1778,8 @@ class SoreideWhitson:
|
|
|
1742
1778
|
y_H2S=self.gas_comp.get('H2S', 0),
|
|
1743
1779
|
y_H2=self.gas_comp.get('H2', 0),
|
|
1744
1780
|
method='flash',
|
|
1745
|
-
salinity_method=
|
|
1746
|
-
framework=
|
|
1781
|
+
salinity_method=self.salinity_method,
|
|
1782
|
+
framework=self.framework,
|
|
1747
1783
|
)
|
|
1748
1784
|
|
|
1749
1785
|
self.x = x_gas
|
|
@@ -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
|
|
@@ -14,11 +14,11 @@ Returns tuple of (Bw (rb/stb), Density (sg), viscosity (cP), Compressibility (1/
|
|
|
14
14
|
Unit System Support
|
|
15
15
|
----------------------
|
|
16
16
|
|
|
17
|
-
All three brine models
|
|
17
|
+
All three brine models default to oilfield units (psia, degF, 1/psi, scf/stb) and accept ``metric=True`` to switch to Eclipse METRIC (barsa, degC, 1/bar, sm3/sm3):
|
|
18
18
|
|
|
19
|
-
- ``brine_props``: ``metric=False`` (default).
|
|
20
|
-
- ``CO2_Brine_Mixture``: ``metric=
|
|
21
|
-
- ``SoreideWhitson``: ``metric=
|
|
19
|
+
- ``brine_props``: ``metric=False`` (default).
|
|
20
|
+
- ``CO2_Brine_Mixture``: ``metric=False`` (default).
|
|
21
|
+
- ``SoreideWhitson``: ``metric=False`` (default).
|
|
22
22
|
|
|
23
23
|
.. note::
|
|
24
24
|
|
|
@@ -126,7 +126,7 @@ pyrestoolbox.brine.CO2_Brine_Mixture
|
|
|
126
126
|
|
|
127
127
|
.. code-block:: python
|
|
128
128
|
|
|
129
|
-
CO2_Brine_Mixture(pres, temp, ppm = 0, metric =
|
|
129
|
+
CO2_Brine_Mixture(pres, temp, ppm = 0, metric = False) -> class
|
|
130
130
|
|
|
131
131
|
.. list-table:: Inputs
|
|
132
132
|
:widths: 10 15 40
|
|
@@ -211,7 +211,7 @@ Usage example for 175 Bara x 85 degC and 0% NaCl brine:
|
|
|
211
211
|
|
|
212
212
|
.. code-block:: python
|
|
213
213
|
|
|
214
|
-
>>> mix = brine.CO2_Brine_Mixture(pres = 175, temp = 85)
|
|
214
|
+
>>> mix = brine.CO2_Brine_Mixture(pres = 175, temp = 85, metric = True)
|
|
215
215
|
>>> mix.Rs # Returns sm3 dissolved CO2 / sm3 Brine
|
|
216
216
|
24.743651168969475
|
|
217
217
|
|
|
@@ -312,7 +312,7 @@ pyrestoolbox.brine.SoreideWhitson
|
|
|
312
312
|
|
|
313
313
|
.. code-block:: python
|
|
314
314
|
|
|
315
|
-
SoreideWhitson(pres, temp, ppm=0, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0, sg=0.65, metric=
|
|
315
|
+
SoreideWhitson(pres, temp, ppm=0, y_CO2=0, y_H2S=0, y_N2=0, y_H2=0, sg=0.65, metric=False, cw_sat=False, framework='proposed', salinity_method='gamma_phi') -> class
|
|
316
316
|
|
|
317
317
|
Soreide-Whitson (1992) VLE model for multicomponent gas solubility in water/brine, with Garcia/Plyasunov
|
|
318
318
|
density corrections and calibrated viscosity corrections. Supports gas mixtures containing any combination
|
|
@@ -377,6 +377,12 @@ based on the gas specific gravity using constrained exponential decay to match t
|
|
|
377
377
|
* - cw_sat
|
|
378
378
|
- Boolean
|
|
379
379
|
- If True, will also calculate saturated brine compressibility. Default False
|
|
380
|
+
* - framework
|
|
381
|
+
- str
|
|
382
|
+
- VLE framework used by the S&W engine. ``'proposed'`` (default, Soreide-Whitson 1992 re-fit), ``'sw_original'`` (original 1992 published coefficients), or ``'dropin'`` (PR-EOS fitted with brine-aware water alpha). Affects kij and ks correlations.
|
|
383
|
+
* - salinity_method
|
|
384
|
+
- str
|
|
385
|
+
- How salinity enters the flash. ``'gamma_phi'`` (default, Sechenov salting-out via activity coefficient), ``'embedded'`` (salinity inside kij — only compatible with ``'dropin'``/``'sw_original'``), ``'explicit'`` (brine treated as a component in the flash), ``'sechenov'`` (alias for ``gamma_phi``), or ``'auto'`` (pick per-gas default). ``framework='proposed'`` + ``salinity_method='embedded'`` emits a warning and falls back to ``gamma_phi``.
|
|
380
386
|
|
|
381
387
|
.. list-table:: Returns (SoreideWhitson)
|
|
382
388
|
:widths: 10 15 40
|
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
Changelist in 3.4.0:
|
|
2
|
+
|
|
3
|
+
- **BREAKING — Brine metric default standardization**: ``CO2_Brine_Mixture`` and ``SoreideWhitson`` constructors now default to ``metric=False`` (oilfield units) to match ``brine_props`` and every other pyrestoolbox API. Previously they defaulted to ``metric=True``. Existing callers that relied on the old default must either pass ``metric=True`` explicitly or switch their input units to psia / degF. RST and notebook examples that relied on the default have been updated to pass ``metric=True`` explicitly.
|
|
4
|
+
|
|
5
|
+
- **nodal.outflow_curve parameter unification**: ``n_rates`` has been renamed to ``n_points`` to match ``ipr_curve`` / ``operating_point``. ``n_rates`` remains accepted as a deprecated alias (takes precedence when both are passed) — existing callers keep working without changes.
|
|
6
|
+
|
|
7
|
+
- **Type hints on public APIs**: ``nodal`` (fbhp / fthp / outflow_curve / ipr_curve / operating_point), ``oil`` (oil_co/bt return types, OilPVT constructor + methods, oil_rate_radial/linear), and ``dca`` (arps_*, duong_*, eur, fit_*, ratio_forecast, forecast) public signatures now carry type annotations. IDE autocomplete and static analysis now work out of the box for the primary user surface. ``matbal`` / ``simtools`` / ``plyasunov`` type-hint backfill deferred.
|
|
8
|
+
|
|
9
|
+
- **Rust-vs-Python parity harness**: 63 new parametrized tests added to ``test_rust_acceleration.py``. Coverage:
|
|
10
|
+
|
|
11
|
+
- Nodal gas VLP: 4 methods × 6 (THP, rate) points on a vertical well; WG/BB × 2 points on a deviated well (45-degree lower segment).
|
|
12
|
+
- Nodal oil VLP: 4 methods × 5 (THP, rate) points on a vertical well; WG/BB × 2 points on a deviated well.
|
|
13
|
+
- SoreideWhitson: pure CO2 across 4 (P, T) × 3 salinity points; sour-gas mix (CO2 + H2S + N2) at 2 points; natural-gas-only at 2 points.
|
|
14
|
+
|
|
15
|
+
Purpose is prophylactic — catches future one-sided edits between Python and Rust. Tolerance is ``RTOL_MEDIUM = 1e-4``.
|
|
16
|
+
|
|
17
|
+
- **Rust HB-oil VLP bugfix (correctness)**. Three dimensional bugs in ``src/vlp/segment_oil.rs`` (HB oil only — WG/GRAY/BB oil were coded correctly):
|
|
18
|
+
|
|
19
|
+
1. ``min_l`` formula (``rho_g * 62.37 / (rho_g * 62.37 + lsg * 62.37)`` simplified to ``rho_g_lbft3 / (rho_g_lbft3 + lsg_SG)``) mixed lb/cuft with dimensionless SG. Forced ~35% minimum liquid holdup at low rate when Python computed ~2%. Dominant contributor to the divergence.
|
|
20
|
+
2. ``rho_avg`` averaged gas vs stock-tank oil density, not the oil+water mixture density that includes live-oil McCain correction and water cut.
|
|
21
|
+
3. ``ul`` superficial liquid velocity used stock-tank volumetric flow (``ql * 5.615 / 86400 / area``) instead of reservoir-conditions volumetric (``mflow_l / rho_l / area``). Missed the live-oil expansion / water-cut weighting.
|
|
22
|
+
|
|
23
|
+
Combined effect was up to ~75% over-prediction of BHP for HB oil wells at low rate / low THP. Python path was always correct; bug was isolated to the Rust accelerator. The ``hb_holdup`` helper signature changed from taking ``lsg`` (implicit ``62.4 * lsg`` internally) to taking ``rho_l`` directly; gas-segment callers updated to pass ``62.4 * lsg_loc`` to preserve prior behaviour. Parity harness drift reduced from 75% to <0.1% on the affected grid.
|
|
24
|
+
|
|
25
|
+
- **Nodal ``operating_point`` non-monotonic VLP handling**. HB oil VLP curves can be non-monotonic at very low rate (spurious near-shut-in high BHP from holdup correlations). The previous bisection bracketed ``[min_rate, AOF]`` and failed when both endpoints had the same sign despite a crossing existing in between. Replaced with a scan-then-bisect: sample 25 rates, locate all sign changes in the error function, and bisect in the highest-rate bracket — the physical operating point. Sets ``converged=False`` if no sign change is found.
|
|
26
|
+
|
|
27
|
+
- **HB oil doc-example values updated**. Three expected values changed due to the Rust HB-oil bugfix (Python values unchanged, Rust values now match Python):
|
|
28
|
+
|
|
29
|
+
- ``fbhp(thp=200, ..., vlpmethod='HB', well_type='oil', qt_stbpd=2000, ...)``: 2271.72 psi → 1771.47 psi
|
|
30
|
+
- ``fbhp`` with ``oil_pvt=opvt``: 2273.72 psi → 1772.24 psi
|
|
31
|
+
- ``operating_point`` (HB oil, thp=200, reservoir pr=3000): rate 1391.4 → 2019.0 stb/d, bhp 2206.3 → 1778.5 psi
|
|
32
|
+
|
|
33
|
+
RST and notebook examples updated to match the corrected values.
|
|
34
|
+
|
|
35
|
+
- **nodal.fthp**: New reverse-solve function. ``fthp(bhp, completion, ...)`` returns the tubing head pressure that produces the specified BHP under the given VLP correlation. Wraps ``bisect_solve`` over ``fbhp``, accepts the same flow/well parameters as ``fbhp``, and has matching units/metric handling. Users no longer need to hand-roll a bisection for wellhead back-calculation workflows.
|
|
36
|
+
|
|
37
|
+
- **nodal.fbhp ``return_profile=True``**: New kwarg. When enabled, ``fbhp`` returns a ``NodalResult`` with per-segment-boundary ``md``, ``tvd``, and ``p`` arrays (plus scalar ``bhp``) instead of just the bottom-hole scalar. Exposes the wellbore pressure traverse without forking the internal segment march. Metric-aware outputs.
|
|
38
|
+
|
|
39
|
+
- **nodal.operating_point extensions**:
|
|
40
|
+
|
|
41
|
+
- New ``injection=False`` kwarg, forwarded to internal ``fbhp`` and ``outflow_curve`` calls so injection wells can be solved directly through ``operating_point``. Previously ``injection=True`` was honoured only by direct ``fbhp`` calls.
|
|
42
|
+
- New ``converged`` key in the returned ``NodalResult``. ``True`` when the VLP/IPR bisection succeeded, ``False`` when it fell back to ``rate=0, bhp=pr`` (no intersection). Surfaces a condition that previously looked like a plausible zero-rate answer.
|
|
43
|
+
|
|
44
|
+
- **nodal.outflow_curve dict key**: Returned ``NodalResult`` now carries both ``'rate'`` and ``'rates'`` entries (same list). ``'rate'`` matches ``ipr_curve`` and ``operating_point``; ``'rates'`` is kept for backward compatibility.
|
|
45
|
+
|
|
46
|
+
- **SoreideWhitson framework / salinity_method exposed**: New ``framework`` (``'proposed'`` | ``'sw_original'`` | ``'dropin'``) and ``salinity_method`` (``'gamma_phi'`` | ``'embedded'`` | ``'explicit'`` | ``'sechenov'`` | ``'auto'``) constructor kwargs, forwarded to the VLE engine. Defaults unchanged (``'proposed'`` + ``'gamma_phi'``). Combining ``framework='proposed'`` with ``salinity_method='embedded'`` now emits a warning because the engine falls back to ``gamma_phi`` for the ``proposed`` kij set.
|
|
47
|
+
|
|
48
|
+
- **Brine ``ppm`` / ``wt`` conflict detection**: ``brine_props``, ``CO2_Brine_Mixture``, and ``SoreideWhitson`` now raise ``ValueError("Supply either ... not both.")`` when both aliases are passed. Previously one silently won with no feedback.
|
|
49
|
+
|
|
50
|
+
- **Oil API cleanup**:
|
|
51
|
+
|
|
52
|
+
- Removed private-helper re-exports ``_cofb_mccain``, ``_perrine_co_sat``, ``_resolve_pb_rsb``, ``_build_bot_tables``, ``_format_bot_results`` from ``pyrestoolbox.oil`` public namespace. Still reachable via ``pyrestoolbox.oil._tables`` / ``._density`` / ``._compressibility`` for advanced users; ``simtools.make_bot_og`` updated internally.
|
|
53
|
+
- Dead imports removed across ``_compressibility``, ``_correlations``, ``_density``, ``_harmonize``, ``_tables`` sub-files (11 symbols total).
|
|
54
|
+
- Valko-McCain (2003) coefficient matrices for ``sg_st_gas`` and ``oil_rs_st`` extracted to ``_constants.py`` with paper citations, per CLAUDE.md named-constants rule.
|
|
55
|
+
|
|
56
|
+
- **Oil documentation sync**: ``docs/oil.rst`` updated — ``oil_deno`` parameter listed as ``sg_o`` (matches code; was incorrectly ``sg_sto``). ``make_bot_og`` result-dict table now documents ``vis_frac`` (was previously omitted despite being returned).
|
|
57
|
+
|
|
58
|
+
- **DCA internals**: 20 previously inline magic numbers extracted to named constants at the top of ``dca.py`` (Arps b-grid 0.05/0.96/0.01, Duong trap bounds and curve_fit bounds, logistic fit bounds, hyperbolic numerical floor). ``_build_decline_result`` helper deduplicates R-squared/residual/DeclineResult construction across the six ``_fit_*`` helpers (~60 lines removed).
|
|
59
|
+
|
|
60
|
+
- 726 validation tests (up from 716 in 3.3.0). New coverage for ``fthp`` roundtrip, ``fbhp`` pressure-traverse return, ``operating_point.converged``, ``SoreideWhitson`` framework/salinity_method validation, and the three ``ppm``/``wt`` conflict raises.
|
|
61
|
+
|
|
1
62
|
Changelist in 3.3.0:
|
|
2
63
|
|
|
3
64
|
- **BNS Z-factor / critical-property coupling**: When either ``zmethod`` or ``cmethod`` is ``BNS``, both are now forced to ``BNS`` for thermodynamic consistency with a ``UserWarning`` naming the overruled counterpart. ``h2 > 0`` continues to auto-select BNS silently. Non-BNS methods (e.g. ``DAK`` + ``SUT``) remain freely mixable. Implemented via a single ``_resolve_methods`` helper applied at all gas public entry points plus ``GasPVT.__init__``.
|