pyrestoolbox 2.2__tar.gz → 2.2.1__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-2.2/pyrestoolbox.egg-info → pyrestoolbox-2.2.1}/PKG-INFO +1 -1
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/brine/brine.py +136 -13
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/brine.rst +102 -1
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/changelist.rst +5 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/gas/gas.py +216 -174
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/shared_fns/shared_fns.py +77 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/test_brine.py +57 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1/pyrestoolbox.egg-info}/PKG-INFO +1 -1
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/setup.cfg +1 -1
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/setup.py +1 -1
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/LICENSE +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/MANIFEST.in +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/README.md +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/README.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyproject.toml +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/brine/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/classes/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/classes/classes.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/constants/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/constants/constants.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/gas.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/bot.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/bot_img.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/dry_gas.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/influence.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/properties_df.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/sgof.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/img/swof.png +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/layer.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/library.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/oil.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/docs/simtools.rst +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/gas/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/layer/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/layer/layer.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/library/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/library/component_library.xlsx +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/library/library.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/oil/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/oil/oil.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/shared_fns/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/simtools/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/simtools/simtools.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/run_all_tests.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/test_gas.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/test_layer.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/test_oil.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/tests/test_simtools.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/validate/__init__.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox/validate/validate.py +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox.egg-info/SOURCES.txt +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox.egg-info/dependency_links.txt +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox.egg-info/requires.txt +0 -0
- {pyrestoolbox-2.2 → pyrestoolbox-2.2.1}/pyrestoolbox.egg-info/top_level.txt +0 -0
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import numpy.typing as npt
|
|
26
|
+
import pandas as pd
|
|
26
27
|
|
|
27
28
|
from typing import Tuple
|
|
29
|
+
from tabulate import tabulate
|
|
28
30
|
|
|
29
31
|
import pyrestoolbox.gas as gas # Needed for Z-Factor
|
|
30
32
|
from pyrestoolbox.classes import z_method, c_method, pb_method, rs_method, bo_method, uo_method, deno_method, co_method, kr_family, kr_table, class_dic
|
|
31
|
-
from pyrestoolbox.shared_fns import convert_to_numpy, process_output
|
|
33
|
+
from pyrestoolbox.shared_fns import convert_to_numpy, process_output, halley_solve_cubic
|
|
32
34
|
from pyrestoolbox.validate import validate_methods
|
|
33
35
|
from pyrestoolbox.constants import R, psc, tsc, degF2R, tscr, scf_per_mol, CUFTperBBL, WDEN, MW_CO2, MW_H2S, MW_N2, MW_AIR, MW_H2
|
|
34
36
|
|
|
@@ -719,31 +721,40 @@ class CO2_Brine_Mixture():
|
|
|
719
721
|
# Cubic Polynomial Solver: f(Z) = Z**3 + E2*Z**2 + E1*Z + E1 = 0
|
|
720
722
|
#=======================================================================
|
|
721
723
|
self.repeat = False
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
724
|
+
|
|
725
|
+
# Try Halley for all roots
|
|
726
|
+
roots = halley_solve_cubic(e2, e1, e0, flag=0)
|
|
727
|
+
|
|
728
|
+
# Fallback to np.roots if Halley returned None
|
|
729
|
+
if roots is None:
|
|
730
|
+
Z = np.roots(np.array([1.0, e2, e1, e0]))
|
|
731
|
+
Z = np.array([x for x in Z if np.isreal(x)]) # Keep only real results
|
|
732
|
+
roots = np.real(Z)
|
|
733
|
+
|
|
734
|
+
if len(roots) > 1: # Evaluate which root to use per Eqs 25 and 26 in Spycher & Pruess (2003)
|
|
735
|
+
vgas, vliq = max(roots), min(roots)
|
|
736
|
+
|
|
727
737
|
w1 = self.pBar*(vgas - vliq)
|
|
728
738
|
w2 = RGASCON * self.tKel * np.log((vgas - self.bMix)/(vliq - self.bMix)) + self.aMix/(self.tKel**0.5 * self.bMix) * np.log((vgas + self.bMix) * vliq / ((vliq + self.bMix) * vgas))
|
|
729
|
-
|
|
739
|
+
|
|
730
740
|
if w2 - w1 > 0:
|
|
731
|
-
|
|
741
|
+
result = max(roots)
|
|
732
742
|
if self.CO2_sat: # CO2 was saturated in previous iteration, but now its not
|
|
733
743
|
self.CO2_sat = False
|
|
734
744
|
self.repeat = True
|
|
735
745
|
else:
|
|
736
|
-
|
|
746
|
+
result = min(roots)
|
|
737
747
|
if not self.CO2_sat:
|
|
738
748
|
self.CO2_sat = True
|
|
739
749
|
self.repeat = True
|
|
740
750
|
else:
|
|
751
|
+
result = roots[0]
|
|
741
752
|
if self.CO2_sat: # CO2 was saturated in previous iteration, but now its not
|
|
742
753
|
self.CO2_sat = False
|
|
743
754
|
self.repeat = True
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return np.real(
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
return np.real(result)
|
|
747
758
|
|
|
748
759
|
|
|
749
760
|
def MolarVolume(self):
|
|
@@ -1236,4 +1247,116 @@ class CO2_Brine_Mixture():
|
|
|
1236
1247
|
# Undersaturated compressibility = 1/V dV/dP
|
|
1237
1248
|
c_usat = 1 - sg_CO2_Brine / sg_CO2_Brine_ # 1/Bar
|
|
1238
1249
|
|
|
1239
|
-
return ([sg_CO2_Brine, sg_brine, rhowtp], [cP_CO2_brine, cP_brine, cP_freshwater], viscosblty, [bw, brine_res_vol, bw_freshwater], rs, c_usat)
|
|
1250
|
+
return ([sg_CO2_Brine, sg_brine, rhowtp], [cP_CO2_brine, cP_brine, cP_freshwater], viscosblty, [bw, brine_res_vol, bw_freshwater], rs, c_usat)
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def make_pvtw_table(
|
|
1254
|
+
pi: float,
|
|
1255
|
+
degf: float,
|
|
1256
|
+
wt: float = 0,
|
|
1257
|
+
ch4_sat: float = 0,
|
|
1258
|
+
pmin: float = 500,
|
|
1259
|
+
pmax: float = 10000,
|
|
1260
|
+
nrows: int = 20,
|
|
1261
|
+
export: bool = False,
|
|
1262
|
+
) -> dict:
|
|
1263
|
+
""" Generates a PVTW (water PVT) table over a pressure range using brine_props (Spivey correlation).
|
|
1264
|
+
Follows the pattern of make_bot_og from the oil module.
|
|
1265
|
+
|
|
1266
|
+
pi: Initial (reference) pressure (psia)
|
|
1267
|
+
degf: Temperature (deg F)
|
|
1268
|
+
wt: Salt wt% (0-100), default 0
|
|
1269
|
+
ch4_sat: Degree of methane saturation (0 - 1), default 0
|
|
1270
|
+
pmin: Minimum pressure for table (psia), default 500
|
|
1271
|
+
pmax: Maximum pressure for table (psia), default 10000
|
|
1272
|
+
nrows: Number of rows in table, default 20
|
|
1273
|
+
export: If True, writes PVTW.INC (ECLIPSE keyword) and pvtw_table.xlsx
|
|
1274
|
+
|
|
1275
|
+
Returns dict with keys:
|
|
1276
|
+
table: pandas DataFrame with columns Pressure, Bw, Density, Viscosity, Cw, Rsw
|
|
1277
|
+
pref: Reference pressure (psia)
|
|
1278
|
+
bw_ref: Bw at reference pressure (rb/stb)
|
|
1279
|
+
cw_ref: Compressibility at reference pressure (1/psi)
|
|
1280
|
+
visw_ref: Viscosity at reference pressure (cP)
|
|
1281
|
+
rsw_ref: Rsw at reference pressure (scf/stb)
|
|
1282
|
+
den_ref: Density (sg) at reference pressure
|
|
1283
|
+
"""
|
|
1284
|
+
# Build pressure grid, ensuring pi is included
|
|
1285
|
+
pressures = list(np.linspace(pmin, pmax, nrows))
|
|
1286
|
+
if pi not in pressures:
|
|
1287
|
+
pressures.append(pi)
|
|
1288
|
+
pressures = sorted(set(pressures))
|
|
1289
|
+
|
|
1290
|
+
bws, dens, visws, cws, rsws = [], [], [], [], []
|
|
1291
|
+
for p in pressures:
|
|
1292
|
+
bw, lden, visw, cw, rsw = brine_props(p=p, degf=degf, wt=wt, ch4_sat=ch4_sat)
|
|
1293
|
+
bws.append(bw)
|
|
1294
|
+
dens.append(lden)
|
|
1295
|
+
visws.append(visw)
|
|
1296
|
+
cws.append(cw)
|
|
1297
|
+
rsws.append(rsw)
|
|
1298
|
+
|
|
1299
|
+
df = pd.DataFrame()
|
|
1300
|
+
df["Pressure (psia)"] = pressures
|
|
1301
|
+
df["Bw (rb/stb)"] = bws
|
|
1302
|
+
df["Density (sg)"] = dens
|
|
1303
|
+
df["Viscosity (cP)"] = visws
|
|
1304
|
+
df["Cw (1/psi)"] = cws
|
|
1305
|
+
df["Rsw (scf/stb)"] = rsws
|
|
1306
|
+
|
|
1307
|
+
# Reference properties at pi
|
|
1308
|
+
bw_ref, den_ref, visw_ref, cw_ref, rsw_ref = brine_props(
|
|
1309
|
+
p=pi, degf=degf, wt=wt, ch4_sat=ch4_sat
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
if export:
|
|
1313
|
+
# Write ECLIPSE PVTW keyword
|
|
1314
|
+
# PVTW format: Pref Bw Cw Visw Viscosibility
|
|
1315
|
+
# Viscosibility set to 0 (constant viscosity assumption)
|
|
1316
|
+
pvtw_line = f" {pi:.1f} {bw_ref:.6f} {cw_ref:.6e} {visw_ref:.4f} 0.0"
|
|
1317
|
+
fileout = f"-- Generated by pyResToolbox make_pvtw_table\n"
|
|
1318
|
+
fileout += f"-- Temperature: {degf:.1f} deg F, Salt: {wt:.1f} wt%, CH4 sat: {ch4_sat:.2f}\n"
|
|
1319
|
+
fileout += f"PVTW\n{pvtw_line} /\n"
|
|
1320
|
+
with open("PVTW.INC", "w") as f:
|
|
1321
|
+
f.write(fileout)
|
|
1322
|
+
|
|
1323
|
+
# Write full table to Excel
|
|
1324
|
+
df.to_excel("pvtw_table.xlsx", index=False)
|
|
1325
|
+
|
|
1326
|
+
return {
|
|
1327
|
+
"table": df,
|
|
1328
|
+
"pref": pi,
|
|
1329
|
+
"bw_ref": bw_ref,
|
|
1330
|
+
"cw_ref": cw_ref,
|
|
1331
|
+
"visw_ref": visw_ref,
|
|
1332
|
+
"rsw_ref": rsw_ref,
|
|
1333
|
+
"den_ref": den_ref,
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
class SoreideWhitson:
|
|
1338
|
+
""" Soreide-Whitson (1992) model for gas solubility in water/brine.
|
|
1339
|
+
|
|
1340
|
+
Planned support for multicomponent gas mixtures containing:
|
|
1341
|
+
C1, C2, C3, nC4, CO2, H2S, N2, H2
|
|
1342
|
+
|
|
1343
|
+
Will calculate:
|
|
1344
|
+
- Mole fraction of dissolved gas components in aqueous phase
|
|
1345
|
+
- Mole fraction of vaporised water in gas phase
|
|
1346
|
+
- Water content of gas (stb/mmscf)
|
|
1347
|
+
- Gas solubility in water (scf/stb)
|
|
1348
|
+
|
|
1349
|
+
Supports fresh and saline water (NaCl equivalent).
|
|
1350
|
+
|
|
1351
|
+
Reference:
|
|
1352
|
+
Soreide, I. and Whitson, C.H., "Peng-Robinson Predictions for Hydrocarbons,
|
|
1353
|
+
CO2, N2, and H2S with Pure Water and NaCl Brine", Fluid Phase Equilibria,
|
|
1354
|
+
77, 217-240, 1992.
|
|
1355
|
+
"""
|
|
1356
|
+
|
|
1357
|
+
def __init__(self, **kwargs):
|
|
1358
|
+
raise NotImplementedError(
|
|
1359
|
+
"SoreideWhitson model is not yet implemented. "
|
|
1360
|
+
"Planned support includes multicomponent gas (C1, C2, C3, nC4, CO2, H2S, N2, H2) "
|
|
1361
|
+
"solubility in fresh/saline water using the Soreide-Whitson (1992) PR-EOS approach."
|
|
1362
|
+
)
|
|
@@ -161,5 +161,106 @@ Usage example for 175 Bara x 85 degC and 0% NaCl brine:
|
|
|
161
161
|
|
|
162
162
|
>>> mix = brine.CO2_Brine_Mixture(pres = 175, temp = 85)
|
|
163
163
|
>>> mix.Rs # Returns sm3 dissolved CO2 / sm3 Brine
|
|
164
|
-
24.742923469934272
|
|
164
|
+
24.742923469934272
|
|
165
|
+
|
|
166
|
+
pyrestoolbox.brine.make_pvtw_table
|
|
167
|
+
======================
|
|
168
|
+
|
|
169
|
+
.. code-block:: python
|
|
170
|
+
|
|
171
|
+
make_pvtw_table(pi, degf, wt=0, ch4_sat=0, pmin=500, pmax=10000, nrows=20, export=False) -> dict
|
|
172
|
+
|
|
173
|
+
Generates a PVTW (water PVT) table over a pressure range using the Spivey correlation (brine_props).
|
|
174
|
+
Optionally exports ECLIPSE PVTW keyword file and Excel spreadsheet.
|
|
175
|
+
|
|
176
|
+
.. list-table:: Inputs
|
|
177
|
+
:widths: 10 15 40
|
|
178
|
+
:header-rows: 1
|
|
179
|
+
|
|
180
|
+
* - Parameter
|
|
181
|
+
- Type
|
|
182
|
+
- Description
|
|
183
|
+
* - pi
|
|
184
|
+
- float
|
|
185
|
+
- Initial (reference) pressure (psia)
|
|
186
|
+
* - degf
|
|
187
|
+
- float
|
|
188
|
+
- Temperature (deg F)
|
|
189
|
+
* - wt
|
|
190
|
+
- float
|
|
191
|
+
- Salt weight% in the brine (0 - 100). Default 0
|
|
192
|
+
* - ch4_sat
|
|
193
|
+
- float
|
|
194
|
+
- Degree of methane saturation (0 - 1). Default 0
|
|
195
|
+
* - pmin
|
|
196
|
+
- float
|
|
197
|
+
- Minimum table pressure (psia). Default 500
|
|
198
|
+
* - pmax
|
|
199
|
+
- float
|
|
200
|
+
- Maximum table pressure (psia). Default 10000
|
|
201
|
+
* - nrows
|
|
202
|
+
- int
|
|
203
|
+
- Number of table rows. Default 20
|
|
204
|
+
* - export
|
|
205
|
+
- bool
|
|
206
|
+
- If True, writes PVTW.INC and pvtw_table.xlsx. Default False
|
|
207
|
+
|
|
208
|
+
.. list-table:: Return dict keys
|
|
209
|
+
:widths: 10 15 40
|
|
210
|
+
:header-rows: 1
|
|
211
|
+
|
|
212
|
+
* - Key
|
|
213
|
+
- Type
|
|
214
|
+
- Description
|
|
215
|
+
* - table
|
|
216
|
+
- DataFrame
|
|
217
|
+
- Pressure, Bw, Density, Viscosity, Cw, Rsw
|
|
218
|
+
* - pref
|
|
219
|
+
- float
|
|
220
|
+
- Reference pressure (psia)
|
|
221
|
+
* - bw_ref
|
|
222
|
+
- float
|
|
223
|
+
- Bw at reference pressure (rb/stb)
|
|
224
|
+
* - cw_ref
|
|
225
|
+
- float
|
|
226
|
+
- Compressibility at reference pressure (1/psi)
|
|
227
|
+
* - visw_ref
|
|
228
|
+
- float
|
|
229
|
+
- Viscosity at reference pressure (cP)
|
|
230
|
+
* - rsw_ref
|
|
231
|
+
- float
|
|
232
|
+
- Rsw at reference pressure (scf/stb)
|
|
233
|
+
* - den_ref
|
|
234
|
+
- float
|
|
235
|
+
- Density (sg) at reference pressure
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
|
|
239
|
+
.. code-block:: python
|
|
240
|
+
|
|
241
|
+
>>> from pyrestoolbox import brine
|
|
242
|
+
>>> result = brine.make_pvtw_table(pi=3000, degf=200, wt=0, ch4_sat=0)
|
|
243
|
+
>>> print(result['bw_ref'])
|
|
244
|
+
>>> print(result['table'].head())
|
|
245
|
+
|
|
246
|
+
pyrestoolbox.brine.SoreideWhitson
|
|
247
|
+
======================
|
|
248
|
+
|
|
249
|
+
.. code-block:: python
|
|
250
|
+
|
|
251
|
+
SoreideWhitson(**kwargs) -> raises NotImplementedError
|
|
252
|
+
|
|
253
|
+
Placeholder for the Soreide-Whitson (1992) PR-EOS model for gas solubility in water/brine.
|
|
254
|
+
|
|
255
|
+
Planned support includes multicomponent gas mixtures (C1, C2, C3, nC4, CO2, H2S, N2, H2)
|
|
256
|
+
solubility in fresh and saline water.
|
|
257
|
+
|
|
258
|
+
Planned return values:
|
|
259
|
+
- Mole fraction of dissolved gas components in aqueous phase
|
|
260
|
+
- Mole fraction of vaporised water in gas phase
|
|
261
|
+
- Water content of gas (stb/mmscf)
|
|
262
|
+
- Gas solubility in water (scf/stb)
|
|
263
|
+
|
|
264
|
+
Reference: Soreide, I. and Whitson, C.H., "Peng-Robinson Predictions for Hydrocarbons,
|
|
265
|
+
CO2, N2, and H2S with Pure Water and NaCl Brine", Fluid Phase Equilibria, 77, 217-240, 1992.
|
|
165
266
|
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import numpy.typing as npt
|
|
26
|
-
from scipy.integrate import quad
|
|
27
26
|
from typing import Tuple
|
|
28
27
|
|
|
29
28
|
import pandas as pd
|
|
@@ -32,6 +31,10 @@ from pyrestoolbox.shared_fns import convert_to_numpy, process_output, check_2_in
|
|
|
32
31
|
from pyrestoolbox.validate import validate_methods
|
|
33
32
|
from pyrestoolbox.constants import R, psc, tsc, degF2R, tscr, scf_per_mol, CUFTperBBL, WDEN, MW_CO2, MW_H2S, MW_N2, MW_AIR, MW_H2
|
|
34
33
|
|
|
34
|
+
# Precomputed Gauss-Legendre nodes/weights for pseudopressure integration
|
|
35
|
+
_GL7_NODES, _GL7_WEIGHTS = np.polynomial.legendre.leggauss(7)
|
|
36
|
+
_GL10_NODES, _GL10_WEIGHTS = np.polynomial.legendre.leggauss(10)
|
|
37
|
+
|
|
35
38
|
def gas_rate_radial(
|
|
36
39
|
k: npt.ArrayLike,
|
|
37
40
|
h: npt.ArrayLike,
|
|
@@ -463,6 +466,99 @@ _BNS_OMEGAA = np.array([0.427671, 0.436725, 0.457236, 0.457236, 0.457236])
|
|
|
463
466
|
_BNS_OMEGAB = np.array([0.0696397, 0.0724345, 0.0777961, 0.0777961, 0.0777961])
|
|
464
467
|
_BNS_VCVIS = np.array([1.46352, 1.46808, 1.35526, 0.68473, 0.0]) # cuft/lbmol
|
|
465
468
|
|
|
469
|
+
# BIP precomputed matrices: kij[i,j] = _BIP_CONST[i,j] + _BIP_SLOPE_TC[i,j] / degR
|
|
470
|
+
# Component order: [CO2=0, H2S=1, N2=2, H2=3, Gas=4]
|
|
471
|
+
# Gas column/row uses tpc_hc at runtime; stored slopes in _BIP_GAS_SLOPES
|
|
472
|
+
_BIP_CONST = np.array([
|
|
473
|
+
[ 0. , 0.248638, -0.25 , -0.247153, -0.145561],
|
|
474
|
+
[ 0.248638, 0. , -0.204414, 0. , 0.16852 ],
|
|
475
|
+
[-0.25 , -0.204414, 0. , -0.166253, -0.108 ],
|
|
476
|
+
[-0.247153, 0. , -0.166253, 0. , -0.0620119],
|
|
477
|
+
[-0.145561, 0.16852 , -0.108 , -0.0620119, 0. ]])
|
|
478
|
+
|
|
479
|
+
_BIP_SLOPE_TC = np.array([
|
|
480
|
+
[ 0. , -75.64467996, 63.51120432, 89.65031832, 0.],
|
|
481
|
+
[-75.64467996, 0. , 157.55635404, 0. , 0.],
|
|
482
|
+
[ 63.51120432, 157.55635404, 0. , 17.90313836, 0.],
|
|
483
|
+
[ 89.65031832, 0. , 17.90313836, 0. , 0.],
|
|
484
|
+
[ 0. , 0. , 0. , 0. , 0.]])
|
|
485
|
+
|
|
486
|
+
_BIP_GAS_SLOPES = np.array([0.276572, -0.122378, 0.0605506, 0.0427873])
|
|
487
|
+
|
|
488
|
+
def _calc_bips_fast(degR, tpc_hc):
|
|
489
|
+
"""Compute 5x5 BIP matrix using precomputed constants."""
|
|
490
|
+
slope_tc = _BIP_SLOPE_TC.copy()
|
|
491
|
+
slope_tc[4, :4] = _BIP_GAS_SLOPES * tpc_hc
|
|
492
|
+
slope_tc[:4, 4] = slope_tc[4, :4]
|
|
493
|
+
return _BIP_CONST + slope_tc / degR
|
|
494
|
+
|
|
495
|
+
def _cardano_cubic(c2, c1, c0, flag=0):
|
|
496
|
+
"""Analytic Cardano solver for monic cubic Z^3 + c2*Z^2 + c1*Z + c0 = 0.
|
|
497
|
+
flag=1: max root, flag=-1: min root, flag=0: all real roots."""
|
|
498
|
+
p = (3 * c1 - c2**2) / 3
|
|
499
|
+
q = (2 * c2**3 - 9 * c2 * c1 + 27 * c0) / 27
|
|
500
|
+
root_diagnostic = q**2 / 4 + p**3 / 27
|
|
501
|
+
|
|
502
|
+
if root_diagnostic < 0:
|
|
503
|
+
m = 2 * np.sqrt(-p / 3)
|
|
504
|
+
qpm = 3 * q / p / m
|
|
505
|
+
theta1 = np.arccos(qpm) / 3
|
|
506
|
+
roots = np.array([m * np.cos(theta1),
|
|
507
|
+
m * np.cos(theta1 + 4 * np.pi / 3),
|
|
508
|
+
m * np.cos(theta1 + 2 * np.pi / 3)])
|
|
509
|
+
Zs = roots - c2 / 3
|
|
510
|
+
else:
|
|
511
|
+
P = (-q / 2 + np.sqrt(root_diagnostic))
|
|
512
|
+
if P >= 0:
|
|
513
|
+
P = P ** (1 / 3)
|
|
514
|
+
else:
|
|
515
|
+
P = -(-P) ** (1 / 3)
|
|
516
|
+
Q = (-q / 2 - np.sqrt(root_diagnostic))
|
|
517
|
+
if Q >= 0:
|
|
518
|
+
Q = Q ** (1 / 3)
|
|
519
|
+
else:
|
|
520
|
+
Q = -(-Q) ** (1 / 3)
|
|
521
|
+
Zs = np.array([P + Q]) - c2 / 3
|
|
522
|
+
|
|
523
|
+
if flag == -1:
|
|
524
|
+
return min(Zs)
|
|
525
|
+
if flag == 1:
|
|
526
|
+
return max(Zs)
|
|
527
|
+
return Zs
|
|
528
|
+
|
|
529
|
+
def _halley_cubic_vec(c2, c1, c0, max_iter=50, tol=1e-12):
|
|
530
|
+
"""Vectorized Halley solver: solve Z^3+c2*Z^2+c1*Z+c0=0 for max root (vapor Z).
|
|
531
|
+
c2, c1, c0 are 1D arrays of length N. Returns 1D array of Z values.
|
|
532
|
+
Falls back to _cardano_cubic for any non-converged elements."""
|
|
533
|
+
N = len(c2)
|
|
534
|
+
Z = -c2 / 3.0
|
|
535
|
+
f0 = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
536
|
+
Z = np.where(f0 < 0, Z + 1.0, Z)
|
|
537
|
+
|
|
538
|
+
for _ in range(max_iter):
|
|
539
|
+
f = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
540
|
+
fp = 3.0 * Z**2 + 2.0 * c2 * Z + c1
|
|
541
|
+
fpp = 6.0 * Z + 2.0 * c2
|
|
542
|
+
# Protect against zero derivatives
|
|
543
|
+
safe_fp = np.where(np.abs(fp) < 1e-30, 1e-30, fp)
|
|
544
|
+
dZ = f / safe_fp
|
|
545
|
+
denom = safe_fp - 0.5 * dZ * fpp
|
|
546
|
+
denom = np.where(np.abs(denom) < 1e-30, 1e-30, denom)
|
|
547
|
+
dZ = f / denom
|
|
548
|
+
Z -= dZ
|
|
549
|
+
if np.max(np.abs(dZ)) < tol:
|
|
550
|
+
break
|
|
551
|
+
|
|
552
|
+
# Check residuals and fall back to Cardano for any bad elements
|
|
553
|
+
f = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
554
|
+
bad = np.abs(f) > 1e-6
|
|
555
|
+
if np.any(bad):
|
|
556
|
+
bad_idx = np.where(bad)[0]
|
|
557
|
+
for idx in bad_idx:
|
|
558
|
+
Z[idx] = _cardano_cubic(c2[idx], c1[idx], c0[idx], flag=1)
|
|
559
|
+
|
|
560
|
+
return Z
|
|
561
|
+
|
|
466
562
|
def gas_z(
|
|
467
563
|
p: npt.ArrayLike,
|
|
468
564
|
sg: float,
|
|
@@ -621,103 +717,43 @@ def gas_z(
|
|
|
621
717
|
def z_bur(psias, degf):
|
|
622
718
|
degR = degf + degF2R
|
|
623
719
|
|
|
624
|
-
# Analytic solution for real root(s) of cubic polynomial
|
|
625
|
-
# a[0] * Z**3 + a[1]*Z**2 + a[2]*Z + a[3] = 0
|
|
626
|
-
# Flag = 1 return Max root, = -1 returns minimum root, = 0 returns all real roots
|
|
627
|
-
def cubic_root(a, flag = 0):
|
|
628
|
-
if a[0] != 1:
|
|
629
|
-
a = np.array(a) / a[0] # Normalize to unity exponent for Z^3
|
|
630
|
-
p = (3 * a[2]- a[1]**2)/3
|
|
631
|
-
q = (2 * a[1]**3 - 9 * a[1] * a[2] + 27 * a[3])/27
|
|
632
|
-
root_diagnostic = q**2/4 + p**3/27
|
|
633
|
-
|
|
634
|
-
if root_diagnostic < 0:
|
|
635
|
-
m = 2*np.sqrt(-p/3)
|
|
636
|
-
qpm = 3*q/p/m
|
|
637
|
-
theta1 = np.arccos(qpm)/3
|
|
638
|
-
roots = np.array([m*np.cos(theta1), m*np.cos(theta1+4*np.pi/3), m*np.cos(theta1+2*np.pi/3)])
|
|
639
|
-
Zs = roots - a[1] / 3
|
|
640
|
-
else:
|
|
641
|
-
P = (-q/2 + np.sqrt(root_diagnostic))
|
|
642
|
-
if P >= 0:
|
|
643
|
-
P = P **(1/3)
|
|
644
|
-
else:
|
|
645
|
-
P = -(-P)**(1/3)
|
|
646
|
-
Q = (-q/2 - np.sqrt(root_diagnostic))
|
|
647
|
-
if Q >=0:
|
|
648
|
-
Q = Q **(1/3)
|
|
649
|
-
else:
|
|
650
|
-
Q = -(-Q)**(1/3)
|
|
651
|
-
Zs = np.array([P + Q]) - a[1] / 3
|
|
652
|
-
|
|
653
|
-
if flag == -1: # Return minimum root
|
|
654
|
-
return min(Zs)
|
|
655
|
-
if flag == 1: # Return maximum root
|
|
656
|
-
return max(Zs)
|
|
657
|
-
return Zs # Return all roots
|
|
658
|
-
|
|
659
|
-
def calc_bips(degR, tpc_hc):
|
|
660
|
-
"""
|
|
661
|
-
Temperature-dependent Binary Interaction Parameters (BIPs) for all pairs,
|
|
662
|
-
with analytic first and second T-derivatives for use in PR analytic derivatives.
|
|
663
|
-
- Fitted forms: constant + slope/Tr, Tr based on component crit temp.
|
|
664
|
-
- Returns: kij, dkij_dT, d2kij_dT2 (all NxN)
|
|
665
|
-
- Tuned to experimental VLE data Burgoyne, 2025
|
|
666
|
-
"""
|
|
667
|
-
components = ['CO2', 'H2S', 'N2', 'H2', 'Gas']
|
|
668
|
-
bip_parameters = {
|
|
669
|
-
("Gas", "CO2"): {"constant": -0.145561 , "Tr_slope": 0.276572 , "tc": tpc_hc },
|
|
670
|
-
("Gas", "H2S"): {"constant": 0.16852 , "Tr_slope": -0.122378, "tc": tpc_hc },
|
|
671
|
-
("Gas", "N2"): {"constant": -0.108 , "Tr_slope": 0.0605506, "tc": tpc_hc },
|
|
672
|
-
("Gas", "H2"): {"constant": -0.0620119, "Tr_slope": 0.0427873, "tc": tpc_hc },
|
|
673
|
-
("CO2", "H2S"): {"constant": 0.248638 , "Tr_slope": -0.138185, "tc": 547.416 },
|
|
674
|
-
("CO2", "N2"): {"constant": -0.25 , "Tr_slope": 0.11602 , "tc": 547.416 },
|
|
675
|
-
("CO2", "H2"): {"constant": -0.247153 , "Tr_slope": 0.16377 , "tc": 547.416 },
|
|
676
|
-
("H2S", "N2"): {"constant": -0.204414 , "Tr_slope": 0.234417 , "tc": 672.12 },
|
|
677
|
-
("H2S", "H2"): {"constant": 0 , "Tr_slope": 0 , "tc": 672.12 },
|
|
678
|
-
("N2", "H2"): {"constant": -0.166253 , "Tr_slope": 0.0788129, "tc": 227.16 },
|
|
679
|
-
}
|
|
680
|
-
def lookup_key(i, j):
|
|
681
|
-
return (i, j) if (i, j) in bip_parameters else (j, i)
|
|
682
|
-
n = len(components)
|
|
683
|
-
kij = np.zeros((n, n))
|
|
684
|
-
for i, ci in enumerate(components):
|
|
685
|
-
for j, cj in enumerate(components):
|
|
686
|
-
if ci == cj:
|
|
687
|
-
kij[i, j] = 0.0
|
|
688
|
-
else:
|
|
689
|
-
params = bip_parameters[lookup_key(ci, cj)]
|
|
690
|
-
tc = params["tc"]
|
|
691
|
-
const = params["constant"]
|
|
692
|
-
slope = params["Tr_slope"]
|
|
693
|
-
Tr = degR / tc
|
|
694
|
-
kij[i, j] = const + slope / Tr
|
|
695
|
-
return kij
|
|
696
|
-
|
|
697
720
|
z = np.array([co2, h2s, n2, h2, 1 - co2 - h2s - n2 - h2])
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
# tc_peng, pc_peng = gas_tc_pc(sg, co2, h2s, n2, h2, cmethod = 'BNS')
|
|
701
|
-
|
|
702
|
-
tcs[-1], pcs[-1] = tc, pc # Hydrocarbon Tc and Pc from SG using BNS correlation
|
|
721
|
+
|
|
722
|
+
tcs[-1], pcs[-1] = tc, pc # Hydrocarbon Tc and Pc from SG using BNS correlation
|
|
703
723
|
trs = degR / tcs
|
|
704
|
-
|
|
724
|
+
|
|
705
725
|
m = 0.37464 + 1.54226 * ACF - 0.26992 * ACF**2
|
|
706
|
-
alpha = (1 + m * (1 - np.sqrt(trs)))**2
|
|
707
|
-
|
|
708
|
-
kij =
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
726
|
+
alpha = (1 + m * (1 - np.sqrt(trs)))**2
|
|
727
|
+
|
|
728
|
+
kij = _calc_bips_fast(degR, tc)
|
|
729
|
+
|
|
730
|
+
# Vectorized across all pressures (N = len(psias))
|
|
731
|
+
prs = psias[:, None] / pcs[None, :] # (N, 5)
|
|
732
|
+
Ai = OmegaA * alpha * prs / trs**2 # (N, 5)
|
|
733
|
+
Bi = OmegaB * prs / trs # (N, 5)
|
|
734
|
+
|
|
735
|
+
# Mixing rule A: A = sum_ij z_i*z_j*sqrt(Ai_i*Ai_j)*(1-kij)
|
|
736
|
+
sqrt_Ai = np.sqrt(Ai) # (N, 5)
|
|
737
|
+
w = z * sqrt_Ai # (N, 5)
|
|
738
|
+
onemk = 1.0 - kij # (5, 5)
|
|
739
|
+
A = np.sum((w @ onemk) * w, axis=1) # (N,)
|
|
740
|
+
|
|
741
|
+
# Mixing rule B
|
|
742
|
+
B = Bi @ z # (N,)
|
|
743
|
+
|
|
744
|
+
# Cubic coefficients: Z^3 + c2*Z^2 + c1*Z + c0 = 0
|
|
745
|
+
c2 = -(1.0 - B)
|
|
746
|
+
c1_coeff = A - 3.0 * B**2 - 2.0 * B
|
|
747
|
+
c0 = -(A * B - B**2 - B**3)
|
|
748
|
+
|
|
749
|
+
# Solve all cubics at once
|
|
750
|
+
Z_raw = _halley_cubic_vec(c2, c1_coeff, c0) # (N,)
|
|
751
|
+
|
|
752
|
+
# Volume translation
|
|
753
|
+
vshift = np.sum(z * VSHIFT * Bi, axis=1) # (N,)
|
|
754
|
+
zout = Z_raw - vshift
|
|
755
|
+
|
|
756
|
+
return process_output(zout, is_list)
|
|
721
757
|
|
|
722
758
|
zfuncs = {"DAK": zdak, "HY": z_hy, "WYW": z_wyw, "BUR": z_bur}
|
|
723
759
|
|
|
@@ -790,78 +826,56 @@ def gas_ug(
|
|
|
790
826
|
|
|
791
827
|
rho = m * p / (t * zee * R * 62.37)
|
|
792
828
|
|
|
793
|
-
mws, tcs, pcs = _BNS_MWS.copy(), _BNS_TCS.copy(), _BNS_PCS.copy()
|
|
794
|
-
ACF, VSHIFT = _BNS_ACF, _BNS_VSHIFT
|
|
795
|
-
OmegaA, OmegaB, VCVIS = _BNS_OMEGAA, _BNS_OMEGAB, _BNS_VCVIS
|
|
796
|
-
|
|
797
|
-
# From https://wiki.whitson.com/bopvt/visc_correlations/
|
|
798
|
-
def lbc(Z, degf, psia, sg, co2=0.0, h2s=0.0, n2=0.0, h2 = 0.0):
|
|
799
|
-
if co2 + h2s + n2 + h2 > 1 or co2 < 0 or h2s < 0 or n2 < 0 or h2 < 0:
|
|
800
|
-
return None
|
|
801
|
-
degR = degf + degF2R
|
|
802
|
-
zi = np.array([co2, h2s, n2, h2, 1 - co2 - h2s - n2 - h2])
|
|
803
|
-
if n2 + co2 + h2s + h2 < 1:
|
|
804
|
-
sg_hc = (sg - (co2 * mws[0] + h2s * mws[1] + n2 * mws[2] + h2 * mws[3]) / MW_AIR) / (1 - co2 - h2s - n2 - h2)
|
|
805
|
-
else:
|
|
806
|
-
sg_hc = 0.75 # Irrelevant, since hydrocarbon fraction = 0
|
|
807
|
-
|
|
808
|
-
sg_hc = max(sg_hc, 0.553779772) # Methane is lower limit
|
|
809
|
-
|
|
810
|
-
hc_gas_mw = sg_hc * MW_AIR
|
|
811
|
-
|
|
812
|
-
def vcvis_hc(mw): # Returns hydrocarbon gas VcVis for LBC viscosity calculations
|
|
813
|
-
return 0.0576710 * (mw - 16.0425) + 1.44383 # ft3/lbmol
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
mws[-1] = hc_gas_mw
|
|
817
|
-
tcs[-1], pcs[-1] = gas_tc_pc(hc_gas_mw/MW_AIR, cmethod = 'BNS')
|
|
818
|
-
|
|
819
|
-
VCVIS[-1] = vcvis_hc(hc_gas_mw)
|
|
820
|
-
degR = degf + degF2R
|
|
821
|
-
|
|
822
|
-
def stiel_thodos(degR, mws):
|
|
823
|
-
#Calculate the viscosity of a pure component using the Stiel-Thodos correlation.
|
|
824
|
-
Tr = degR / tcs
|
|
825
|
-
ui = []
|
|
826
|
-
Tc = tcs * 5/9 # (deg K)
|
|
827
|
-
Pc = pcs / 14.696
|
|
828
|
-
eta = Tc**(1/6) / (mws**(1/2) * Pc**(2/3)) # Tc and Pc must be in degK and Atm respectively
|
|
829
|
-
|
|
830
|
-
for i in range(len(Tr)):
|
|
831
|
-
if Tr[i] <= 1.5:
|
|
832
|
-
ui.append(34e-5 * Tr[i]**0.94 / eta[i])
|
|
833
|
-
else:
|
|
834
|
-
ui.append(17.78e-5 * (4.58 * Tr[i] - 1.67)**(5/8) / eta[i])
|
|
835
|
-
return np.array(ui)
|
|
836
|
-
|
|
837
|
-
def u0(zi, ui, mws, Z): # dilute gas mixture viscosity from Herning and Zippener
|
|
838
|
-
sqrt_mws = np.sqrt(mws)
|
|
839
|
-
return np.sum(zi * ui * sqrt_mws)/np.sum(zi * sqrt_mws)
|
|
840
|
-
|
|
841
|
-
a = [0.1023, 0.023364, 0.058533, -0.0392852, 0.00926279] # P3 and P4 have been modified
|
|
842
|
-
# Calculate the viscosity of the mixture using the Lorenz-Bray-Clark method.
|
|
843
|
-
rhoc = 1/np.sum(VCVIS*zi)
|
|
844
|
-
Tc = tcs * 5/9 # (deg K)
|
|
845
|
-
Pc = pcs / 14.696 # (Atm)
|
|
846
|
-
|
|
847
|
-
eta = np.abs(np.sum(zi*Tc)**(1/6)) / (np.abs(np.sum(zi * mws))**0.5 * np.abs(np.sum(zi * Pc))**(2/3)) # Note 0.5 exponent from original paper
|
|
848
|
-
mw = np.sum(zi * mws)
|
|
849
|
-
rhor = psia / (Z * R * degR) / rhoc
|
|
850
|
-
lhs = a[0] + a[1]*rhor + a[2]*rhor**2 + a[3]*rhor**3 + a[4]*rhor**4
|
|
851
|
-
ui = stiel_thodos(degR, mws)
|
|
852
|
-
vis = (lhs**4 - 1e-4)/eta + u0(zi, ui, mws, Z)
|
|
853
|
-
return process_output(vis, is_list)
|
|
854
|
-
|
|
855
829
|
if zmethod.name not in ('BNS', 'BUR'):
|
|
856
830
|
b = 3.448 + (986.4 / t) + (0.01009 * m) # 2.16
|
|
857
831
|
c = 2.447 - (0.2224 * b) # 2.17
|
|
858
832
|
a = ((9.379 + (0.01607 * m)) * np.power(t, 1.5) / (209.2 + (19.26 * m) + t)) # 2.15
|
|
859
833
|
ug = process_output(a * 0.0001 * np.exp(b * np.power(rho, c)), is_list) # 2.14
|
|
860
834
|
else:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
835
|
+
# Vectorized LBC viscosity for BNS method
|
|
836
|
+
# From https://wiki.whitson.com/bopvt/visc_correlations/
|
|
837
|
+
mws_lbc = _BNS_MWS.copy()
|
|
838
|
+
tcs_lbc = _BNS_TCS.copy()
|
|
839
|
+
pcs_lbc = _BNS_PCS.copy()
|
|
840
|
+
VCVIS_lbc = _BNS_VCVIS.copy() # Copy to avoid mutating module-level array
|
|
841
|
+
|
|
842
|
+
degR = degf + degF2R
|
|
843
|
+
zi = np.array([co2, h2s, n2, h2, 1 - co2 - h2s - n2 - h2])
|
|
844
|
+
if n2 + co2 + h2s + h2 < 1:
|
|
845
|
+
sg_hc = (sg - (co2 * mws_lbc[0] + h2s * mws_lbc[1] + n2 * mws_lbc[2] + h2 * mws_lbc[3]) / MW_AIR) / (1 - co2 - h2s - n2 - h2)
|
|
846
|
+
else:
|
|
847
|
+
sg_hc = 0.75
|
|
848
|
+
|
|
849
|
+
sg_hc = max(sg_hc, 0.553779772)
|
|
850
|
+
hc_gas_mw = sg_hc * MW_AIR
|
|
851
|
+
|
|
852
|
+
mws_lbc[-1] = hc_gas_mw
|
|
853
|
+
tcs_lbc[-1], pcs_lbc[-1] = gas_tc_pc(hc_gas_mw / MW_AIR, cmethod='BNS')
|
|
854
|
+
VCVIS_lbc[-1] = 0.0576710 * (hc_gas_mw - 16.0425) + 1.44383
|
|
855
|
+
|
|
856
|
+
# Vectorized Stiel-Thodos
|
|
857
|
+
Tr = degR / tcs_lbc
|
|
858
|
+
Tc_K = tcs_lbc * 5.0 / 9.0
|
|
859
|
+
Pc_atm = pcs_lbc / 14.696
|
|
860
|
+
eta_st = Tc_K**(1.0/6.0) / (mws_lbc**0.5 * Pc_atm**(2.0/3.0))
|
|
861
|
+
ui_low = 34e-5 * Tr**0.94 / eta_st
|
|
862
|
+
ui_high = 17.78e-5 * np.maximum(4.58 * Tr - 1.67, 1e-30)**(5.0/8.0) / eta_st
|
|
863
|
+
ui = np.where(Tr <= 1.5, ui_low, ui_high)
|
|
864
|
+
|
|
865
|
+
# Herning-Zippener dilute gas mixture viscosity
|
|
866
|
+
sqrt_mws = np.sqrt(mws_lbc)
|
|
867
|
+
u0_val = np.sum(zi * ui * sqrt_mws) / np.sum(zi * sqrt_mws)
|
|
868
|
+
|
|
869
|
+
# LBC mixture parameters
|
|
870
|
+
a_lbc = np.array([0.1023, 0.023364, 0.058533, -0.0392852, 0.00926279])
|
|
871
|
+
rhoc = 1.0 / np.sum(VCVIS_lbc * zi)
|
|
872
|
+
eta_mix = np.abs(np.sum(zi * Tc_K))**(1.0/6.0) / (np.abs(np.sum(zi * mws_lbc))**0.5 * np.abs(np.sum(zi * Pc_atm))**(2.0/3.0))
|
|
873
|
+
|
|
874
|
+
# Vectorized over pressures
|
|
875
|
+
zee_arr, _ = convert_to_numpy(zee)
|
|
876
|
+
rhor = p / (zee_arr * R * degR * rhoc)
|
|
877
|
+
lhs = a_lbc[0] + rhor * (a_lbc[1] + rhor * (a_lbc[2] + rhor * (a_lbc[3] + rhor * a_lbc[4])))
|
|
878
|
+
ug = process_output((lhs**4 - 1e-4) / eta_mix + u0_val, is_list)
|
|
865
879
|
if ugz:
|
|
866
880
|
return process_output(ug * zee, is_list)
|
|
867
881
|
else:
|
|
@@ -1191,23 +1205,51 @@ def gas_dmp(
|
|
|
1191
1205
|
tc: Critical gas temperature (deg R). Calculates using cmethod if not specified
|
|
1192
1206
|
pc: Critical gas pressure (psia). Calculates using cmethod if not specified
|
|
1193
1207
|
"""
|
|
1208
|
+
if p1 == p2:
|
|
1209
|
+
return 0
|
|
1210
|
+
|
|
1194
1211
|
if h2 > 0:
|
|
1195
1212
|
cmethod = 'BNS' # The BNS PR EOS method is the only one that can handle Hydrogen
|
|
1196
|
-
zmethod = 'BNS'
|
|
1197
|
-
|
|
1198
|
-
zmethod, cmethod = validate_methods(["zmethod", "cmethod"], [zmethod, cmethod])
|
|
1199
|
-
|
|
1200
|
-
def m_p(p, *args):
|
|
1201
|
-
# Pseudo pressure function to be integrated
|
|
1202
|
-
degf, sg, zmethod, cmethod, tc, pc, n2, co2, h2s, h2 = args
|
|
1203
|
-
zee = gas_z(p=p, degf=degf, sg=sg, zmethod=zmethod, cmethod=cmethod, co2=co2, h2s=h2s, n2=n2, h2 = h2, tc=tc, pc=pc)
|
|
1204
|
-
mugz = gas_ug(p, sg, degf, zmethod, cmethod, co2, h2s, n2, h2, tc, pc, zee, ugz=True) # Gas viscosity z-factor product using a precalculated Z factor
|
|
1205
|
-
return 2 * p / (mugz)
|
|
1213
|
+
zmethod = 'BNS'
|
|
1206
1214
|
|
|
1207
|
-
|
|
1208
|
-
return 0
|
|
1215
|
+
zmethod, cmethod = validate_methods(["zmethod", "cmethod"], [zmethod, cmethod])
|
|
1209
1216
|
|
|
1210
|
-
|
|
1217
|
+
def _gl_integrate(lo, hi, nodes, weights):
|
|
1218
|
+
"""Batch Gauss-Legendre integration of 2p/(mu*Z) over [lo, hi]."""
|
|
1219
|
+
p_mid = (lo + hi) * 0.5
|
|
1220
|
+
p_half = (hi - lo) * 0.5
|
|
1221
|
+
p_eval = p_mid + p_half * nodes
|
|
1222
|
+
zee = gas_z(p=p_eval, degf=degf, sg=sg, zmethod=zmethod, cmethod=cmethod,
|
|
1223
|
+
co2=co2, h2s=h2s, n2=n2, h2=h2, tc=tc, pc=pc)
|
|
1224
|
+
mugz = gas_ug(p_eval, sg, degf, zmethod, cmethod, co2, h2s, n2, h2, tc, pc, zee, ugz=True)
|
|
1225
|
+
return p_half * np.sum(weights * 2.0 * p_eval / mugz)
|
|
1226
|
+
|
|
1227
|
+
# Two-tier integration: compute with n=7 and n=10, compare for convergence
|
|
1228
|
+
result_7 = _gl_integrate(p1, p2, _GL7_NODES, _GL7_WEIGHTS)
|
|
1229
|
+
result_10 = _gl_integrate(p1, p2, _GL10_NODES, _GL10_WEIGHTS)
|
|
1230
|
+
|
|
1231
|
+
if abs(result_10) < 1e-30 or abs(result_10 - result_7) / abs(result_10) < 1e-5:
|
|
1232
|
+
return result_10
|
|
1233
|
+
|
|
1234
|
+
# If not converged, split into two subintervals and integrate with n=10 each (batch)
|
|
1235
|
+
p_mid = (p1 + p2) * 0.5
|
|
1236
|
+
p_half_lo = (p_mid - p1) * 0.5
|
|
1237
|
+
p_half_hi = (p2 - p_mid) * 0.5
|
|
1238
|
+
p_center_lo = (p1 + p_mid) * 0.5
|
|
1239
|
+
p_center_hi = (p_mid + p2) * 0.5
|
|
1240
|
+
|
|
1241
|
+
# Build all evaluation points for both subintervals in a single array
|
|
1242
|
+
p_eval = np.concatenate([p_center_lo + p_half_lo * _GL10_NODES,
|
|
1243
|
+
p_center_hi + p_half_hi * _GL10_NODES])
|
|
1244
|
+
|
|
1245
|
+
zee = gas_z(p=p_eval, degf=degf, sg=sg, zmethod=zmethod, cmethod=cmethod,
|
|
1246
|
+
co2=co2, h2s=h2s, n2=n2, h2=h2, tc=tc, pc=pc)
|
|
1247
|
+
mugz = gas_ug(p_eval, sg, degf, zmethod, cmethod, co2, h2s, n2, h2, tc, pc, zee, ugz=True)
|
|
1248
|
+
integrand = 2.0 * p_eval / mugz
|
|
1249
|
+
|
|
1250
|
+
n = len(_GL10_NODES)
|
|
1251
|
+
return (p_half_lo * np.sum(_GL10_WEIGHTS * integrand[:n]) +
|
|
1252
|
+
p_half_hi * np.sum(_GL10_WEIGHTS * integrand[n:]))
|
|
1211
1253
|
|
|
1212
1254
|
def gas_fws_sg(sg_g: float, cgr: float, api_st: float) -> float:
|
|
1213
1255
|
"""
|
|
@@ -75,6 +75,83 @@ def process_output(input_data, is_list):
|
|
|
75
75
|
# If it's a single value (not in a list or array), return it directly
|
|
76
76
|
return float(input_data)
|
|
77
77
|
|
|
78
|
+
def halley_solve_cubic(c2, c1, c0, flag=1, max_iter=50, tol=1e-12):
|
|
79
|
+
"""Solve Z^3 + c2*Z^2 + c1*Z + c0 = 0 via Halley's method.
|
|
80
|
+
|
|
81
|
+
flag=1: return max real root (vapor)
|
|
82
|
+
flag=-1: return min real root (liquid)
|
|
83
|
+
flag=0: return all real roots as array
|
|
84
|
+
Returns None if not converged (caller should use fallback).
|
|
85
|
+
"""
|
|
86
|
+
# Find one root via Halley iteration from inflection point
|
|
87
|
+
Z = -c2 / 3.0
|
|
88
|
+
f = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
89
|
+
if f < 0:
|
|
90
|
+
Z = Z + 1.0 # Start on vapor side
|
|
91
|
+
|
|
92
|
+
converged = False
|
|
93
|
+
for _ in range(max_iter):
|
|
94
|
+
f = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
95
|
+
fp = 3.0 * Z**2 + 2.0 * c2 * Z + c1
|
|
96
|
+
fpp = 6.0 * Z + 2.0 * c2
|
|
97
|
+
if abs(fp) < 1e-30:
|
|
98
|
+
break
|
|
99
|
+
dZ = f / fp
|
|
100
|
+
denom = fp - 0.5 * dZ * fpp
|
|
101
|
+
if abs(denom) < 1e-30:
|
|
102
|
+
break
|
|
103
|
+
dZ = f / denom
|
|
104
|
+
Z -= dZ
|
|
105
|
+
if abs(dZ) < tol:
|
|
106
|
+
converged = True
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if not converged:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# Check residual
|
|
113
|
+
f = Z**3 + c2 * Z**2 + c1 * Z + c0
|
|
114
|
+
if abs(f) > 1e-6:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
r1 = Z
|
|
118
|
+
|
|
119
|
+
# Factor out converged root via synthetic division: Z^2 + e1*Z + e0
|
|
120
|
+
e1 = c2 + r1
|
|
121
|
+
e0 = c1 + r1 * e1
|
|
122
|
+
|
|
123
|
+
roots = [r1]
|
|
124
|
+
|
|
125
|
+
# Solve depressed quadratic
|
|
126
|
+
disc = e1**2 - 4.0 * e0
|
|
127
|
+
if disc >= 0:
|
|
128
|
+
sqrt_disc = np.sqrt(disc)
|
|
129
|
+
r2 = (-e1 + sqrt_disc) / 2.0
|
|
130
|
+
r3 = (-e1 - sqrt_disc) / 2.0
|
|
131
|
+
|
|
132
|
+
# Refine r2 with one Halley step (Michelsen pattern)
|
|
133
|
+
for r in (r2, r3):
|
|
134
|
+
f = r**3 + c2 * r**2 + c1 * r + c0
|
|
135
|
+
fp = 3.0 * r**2 + 2.0 * c2 * r + c1
|
|
136
|
+
fpp = 6.0 * r + 2.0 * c2
|
|
137
|
+
if abs(fp) > 1e-30:
|
|
138
|
+
dZ = f / fp
|
|
139
|
+
denom = fp - 0.5 * dZ * fpp
|
|
140
|
+
if abs(denom) > 1e-30:
|
|
141
|
+
r -= f / denom
|
|
142
|
+
roots.append(r)
|
|
143
|
+
|
|
144
|
+
roots = np.array(roots)
|
|
145
|
+
|
|
146
|
+
if flag == 0:
|
|
147
|
+
return roots
|
|
148
|
+
elif flag == 1:
|
|
149
|
+
return float(np.max(roots))
|
|
150
|
+
elif flag == -1:
|
|
151
|
+
return float(np.min(roots))
|
|
152
|
+
return roots
|
|
153
|
+
|
|
154
|
+
|
|
78
155
|
def check_2_inputs(x: Union[float, List[float]], y: Union[float, List[float]]) -> bool:
|
|
79
156
|
""" Check inputs that need to be matched, either both float or both lists/arrays of same length"""
|
|
80
157
|
# Check if both are scalars
|
|
@@ -139,6 +139,63 @@ def test_regression_co2_xco2():
|
|
|
139
139
|
assert abs(mix.x[0] - b['co2_xco2_200_80']) < 1e-8
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# make_pvtw_table Tests
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
def test_make_pvtw_table_basic():
|
|
147
|
+
"""Result has expected keys and correct row count"""
|
|
148
|
+
result = brine.make_pvtw_table(pi=3000, degf=200, wt=0, ch4_sat=0, nrows=10)
|
|
149
|
+
expected_keys = {'table', 'pref', 'bw_ref', 'cw_ref', 'visw_ref', 'rsw_ref', 'den_ref'}
|
|
150
|
+
assert expected_keys == set(result.keys()), f"Missing keys: {expected_keys - set(result.keys())}"
|
|
151
|
+
# nrows=10 plus pi if not already in grid
|
|
152
|
+
assert len(result['table']) >= 10, f"Expected at least 10 rows, got {len(result['table'])}"
|
|
153
|
+
assert result['pref'] == 3000, f"pref should be 3000, got {result['pref']}"
|
|
154
|
+
|
|
155
|
+
def test_make_pvtw_table_bw_range():
|
|
156
|
+
"""Bw values should be in reasonable range (0.9 - 1.2)"""
|
|
157
|
+
result = brine.make_pvtw_table(pi=3000, degf=200, wt=0, ch4_sat=0)
|
|
158
|
+
bw_vals = result['table']['Bw (rb/stb)'].values
|
|
159
|
+
assert all(0.9 < bw < 1.2 for bw in bw_vals), f"Bw values outside 0.9-1.2: {bw_vals}"
|
|
160
|
+
|
|
161
|
+
def test_make_pvtw_table_ref_vs_direct():
|
|
162
|
+
"""Reference properties should match direct brine_props() call"""
|
|
163
|
+
pi, degf, wt, ch4_sat = 3000, 200, 5, 0.5
|
|
164
|
+
result = brine.make_pvtw_table(pi=pi, degf=degf, wt=wt, ch4_sat=ch4_sat)
|
|
165
|
+
bw, lden, visw, cw, rsw = brine.brine_props(p=pi, degf=degf, wt=wt, ch4_sat=ch4_sat)
|
|
166
|
+
assert abs(result['bw_ref'] - bw) < 1e-10, f"bw_ref mismatch: {result['bw_ref']} vs {bw}"
|
|
167
|
+
assert abs(result['den_ref'] - lden) < 1e-10, f"den_ref mismatch"
|
|
168
|
+
assert abs(result['visw_ref'] - visw) < 1e-10, f"visw_ref mismatch"
|
|
169
|
+
assert abs(result['cw_ref'] - cw) < 1e-10, f"cw_ref mismatch"
|
|
170
|
+
assert abs(result['rsw_ref'] - rsw) < 1e-10, f"rsw_ref mismatch"
|
|
171
|
+
|
|
172
|
+
def test_make_pvtw_table_pi_included():
|
|
173
|
+
"""pi should appear in the pressure grid"""
|
|
174
|
+
pi = 3333
|
|
175
|
+
result = brine.make_pvtw_table(pi=pi, degf=200, wt=0, ch4_sat=0)
|
|
176
|
+
pressures = result['table']['Pressure (psia)'].values
|
|
177
|
+
assert pi in pressures, f"pi={pi} not found in pressure grid: {pressures}"
|
|
178
|
+
|
|
179
|
+
def test_make_pvtw_table_saline():
|
|
180
|
+
"""Saline brine density should be greater than freshwater density"""
|
|
181
|
+
result_fresh = brine.make_pvtw_table(pi=3000, degf=200, wt=0, ch4_sat=0)
|
|
182
|
+
result_saline = brine.make_pvtw_table(pi=3000, degf=200, wt=10, ch4_sat=0)
|
|
183
|
+
assert result_saline['den_ref'] > result_fresh['den_ref'], \
|
|
184
|
+
f"Saline density ({result_saline['den_ref']}) should exceed freshwater ({result_fresh['den_ref']})"
|
|
185
|
+
|
|
186
|
+
# =============================================================================
|
|
187
|
+
# SoreideWhitson Tests
|
|
188
|
+
# =============================================================================
|
|
189
|
+
|
|
190
|
+
def test_soreide_whitson_not_implemented():
|
|
191
|
+
"""SoreideWhitson should raise NotImplementedError"""
|
|
192
|
+
try:
|
|
193
|
+
brine.SoreideWhitson()
|
|
194
|
+
assert False, "Expected NotImplementedError"
|
|
195
|
+
except NotImplementedError as e:
|
|
196
|
+
assert "not yet implemented" in str(e).lower(), f"Unexpected error message: {e}"
|
|
197
|
+
|
|
198
|
+
|
|
142
199
|
if __name__ == '__main__':
|
|
143
200
|
print("=" * 70)
|
|
144
201
|
print("BRINE MODULE VALIDATION TESTS")
|
|
@@ -12,7 +12,7 @@ with open(os.path.join(ROOT, 'README.md'), 'r', encoding='utf-8') as f:
|
|
|
12
12
|
setup(
|
|
13
13
|
name='pyrestoolbox',
|
|
14
14
|
include_package_data=True,
|
|
15
|
-
version='2.2', # Ideally should be same as your GitHub release tag version
|
|
15
|
+
version='2.2.1', # Ideally should be same as your GitHub release tag version
|
|
16
16
|
packages=find_packages(),
|
|
17
17
|
description='pyResToolbox - A collection of Reservoir Engineering Utilities',
|
|
18
18
|
license="GNU General Public License v3 or later (GPLv3+)",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|