pyrestoolbox 3.2.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.2.0 → pyrestoolbox-3.4.0}/PKG-INFO +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyproject.toml +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/brine.py +46 -10
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/dca/dca.py +124 -93
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/brine.rst +13 -7
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/changelist.rst +76 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/examples.ipynb +16 -19
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/gas.rst +38 -24
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal.rst +55 -7
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/oil.rst +7 -4
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/gas/gas.py +170 -85
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/nodal/nodal.py +170 -47
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/__init__.py +1 -3
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_compressibility.py +4 -6
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_constants.py +18 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_correlations.py +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_density.py +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_harmonize.py +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_pvt_class.py +17 -13
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_rate.py +4 -2
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_separator.py +10 -15
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_tables.py +1 -3
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/simtools/simtools.py +4 -4
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/gas_viscosity/mod.rs +23 -7
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/pseudopressure.rs +36 -20
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_hb.rs +9 -5
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/segment_gas.rs +1 -1
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/segment_oil.rs +30 -5
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/zfactor/mod.rs +33 -7
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/.github/workflows/build-wheels.yml +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/.gitignore +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/Cargo.lock +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/Cargo.toml +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/LICENSE +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/MANIFEST.in +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/README.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/ResToolbox/privacy_policy.md +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/benchmark_rust_vs_python.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/build_pure_python.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/_accelerator.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/brine/_lib_vle_engine.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/classes/classes.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/dca/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/dca.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/matbal.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/recommend.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/sensitivity.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/matbal/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/matbal/matbal.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/nodal/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/oil/_utils.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/plyasunov_model.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/plyasunov/water_properties.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/recommend/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/recommend/recommend.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/sensitivity/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/pyrestoolbox/validate/validate.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/setup.cfg +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/setup.py +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/bessel.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/critical_properties/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/dca/hyperbolic.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/dca/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/dca/ransac.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/gwr.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/lib.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/matbal/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/matbal/objective.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/oil/density.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/oil/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/spycher_pruess/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/spycher_pruess/solubility.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/alpha.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/bip.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/components.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/eos.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/flash.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/fugacity.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/k_init.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vle/rachford_rice.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/friction.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_bb.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_gray.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/holdup_wg.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/ift.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/mod.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/pvt_helpers.rs +0 -0
- {pyrestoolbox-3.2.0 → pyrestoolbox-3.4.0}/src/vlp/static_column.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:
|
|
@@ -315,7 +319,7 @@ def brine_props(p: float = None, degf: float = None, wt: float = None, ch4_sat:
|
|
|
315
319
|
/ ((Mpa - vap_pressure) - 2 * dlambdadptm * m)
|
|
316
320
|
) # Eq 4.33
|
|
317
321
|
|
|
318
|
-
zee = gas.gas_z(p=p, sg=_SG_METHANE, degf=degf, zmethod='BNS',
|
|
322
|
+
zee = gas.gas_z(p=p, sg=_SG_METHANE, degf=degf, zmethod='BNS', cmethod='BNS',
|
|
319
323
|
co2=0, h2s=0, n2=0, h2=0) # Z-Factor of pure methane
|
|
320
324
|
|
|
321
325
|
vmch4g = zee * _R_CM3_MPA * degk / Mpa # Eq 4.34
|
|
@@ -342,7 +346,7 @@ def brine_props(p: float = None, degf: float = None, wt: float = None, ch4_sat:
|
|
|
342
346
|
|
|
343
347
|
|
|
344
348
|
|
|
345
|
-
zee_sc = gas.gas_z(p=psc, sg=_SG_METHANE, degf=tsc, zmethod='BNS',
|
|
349
|
+
zee_sc = gas.gas_z(p=psc, sg=_SG_METHANE, degf=tsc, zmethod='BNS', cmethod='BNS',
|
|
346
350
|
co2=0, h2s=0, n2=0, h2=0)
|
|
347
351
|
vmch4g_sc = zee_sc * _R_CM3_MPA * (273 + 15) / 0.1013 # Eq 4.34
|
|
348
352
|
rsw_new = mch4 * vmch4g_sc / ((1000 + m * 58.4428) * vb0_sc)
|
|
@@ -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
|
|
@@ -1552,7 +1588,7 @@ class SoreideWhitson:
|
|
|
1552
1588
|
self.gas_comp.get(g, 0) * _SW_GAS_MW.get(g, 28.97)
|
|
1553
1589
|
for g in self.gas_comp
|
|
1554
1590
|
) / MW_AIR
|
|
1555
|
-
zee = gas.gas_z(p=self.psia, sg=gas_sg, degf=self.degF, zmethod='BNS',
|
|
1591
|
+
zee = gas.gas_z(p=self.psia, sg=gas_sg, degf=self.degF, zmethod='BNS', cmethod='BNS',
|
|
1556
1592
|
co2=self.gas_comp.get('CO2', 0),
|
|
1557
1593
|
h2s=self.gas_comp.get('H2S', 0),
|
|
1558
1594
|
n2=self.gas_comp.get('N2', 0),
|
|
@@ -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
|