yaeos 4.0.0__cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
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.
- yaeos/__init__.py +56 -0
- yaeos/constants.py +11 -0
- yaeos/core.py +2874 -0
- yaeos/envelopes.py +510 -0
- yaeos/fitting/__init__.py +12 -0
- yaeos/fitting/core.py +199 -0
- yaeos/fitting/model_setters.py +76 -0
- yaeos/fitting/solvers.py +87 -0
- yaeos/gpec.py +225 -0
- yaeos/lib/__init__.py +9 -0
- yaeos/lib/yaeos_python.cpython-313-x86_64-linux-gnu.so +0 -0
- yaeos/models/__init__.py +26 -0
- yaeos/models/excess_gibbs/__init__.py +20 -0
- yaeos/models/excess_gibbs/dortmund.py +45 -0
- yaeos/models/excess_gibbs/nrtl.py +49 -0
- yaeos/models/excess_gibbs/psrk_unifac.py +45 -0
- yaeos/models/excess_gibbs/unifac.py +45 -0
- yaeos/models/excess_gibbs/uniquac.py +117 -0
- yaeos/models/groups.py +49 -0
- yaeos/models/residual_helmholtz/__init__.py +21 -0
- yaeos/models/residual_helmholtz/cubic_eos/__init__.py +38 -0
- yaeos/models/residual_helmholtz/cubic_eos/cubic_eos.py +449 -0
- yaeos/models/residual_helmholtz/cubic_eos/mixing_rules.py +253 -0
- yaeos/models/residual_helmholtz/multifluid/__init__.py +12 -0
- yaeos/models/residual_helmholtz/multifluid/gerg2008.py +73 -0
- yaeos-4.0.0.dist-info/METADATA +87 -0
- yaeos-4.0.0.dist-info/RECORD +34 -0
- yaeos-4.0.0.dist-info/WHEEL +6 -0
- yaeos.libs/libblas-fe34f726.so.3.8.0 +0 -0
- yaeos.libs/libgfortran-8f1e9814.so.5.0.0 +0 -0
- yaeos.libs/libgomp-870cb1d0.so.1.0.0 +0 -0
- yaeos.libs/liblapack-31d7d384.so.3.8.0 +0 -0
- yaeos.libs/libmvec-2-8eb5c230.28.so +0 -0
- yaeos.libs/libquadmath-828275a7.so.0.0.0 +0 -0
yaeos/fitting/core.py
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
"""Yaeos fitting core module."""
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from scipy.optimize import minimize
|
6
|
+
|
7
|
+
from yaeos.fitting.solvers import solve_pt
|
8
|
+
|
9
|
+
|
10
|
+
class BinaryFitter:
|
11
|
+
"""BinaryFitter class.
|
12
|
+
|
13
|
+
This class is used to fit binary interaction parameters to experimental
|
14
|
+
data. The objective function is defined as the sum of the squared errors
|
15
|
+
between the experimental data and the model predictions.
|
16
|
+
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
model_setter : callable
|
20
|
+
A function that returns a model object. The function should take the
|
21
|
+
optimization parameters as the first argument and any other arguments
|
22
|
+
as the following arguments.
|
23
|
+
model_setter_args : tuple
|
24
|
+
A tuple with the arguments to pass to the model_setter function.
|
25
|
+
data : pandas.DataFrame
|
26
|
+
A DataFrame with the experimental data.
|
27
|
+
The DataFrame should have the following columns:
|
28
|
+
- kind: str, the kind of data point (bubble, dew, liquid-liquid, PT,
|
29
|
+
critical)
|
30
|
+
- x1: float, the mole fraction of component 1
|
31
|
+
- y1: float, the mole fraction of component 1
|
32
|
+
- T: float, the temperature in K
|
33
|
+
- P: float, the pressure in bar
|
34
|
+
verbose : bool, optional
|
35
|
+
If True, print the objective function value and the optimization
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, model_setter, model_setter_args, data, verbose=False):
|
39
|
+
self._get_model = model_setter
|
40
|
+
self._get_model_args = model_setter_args
|
41
|
+
self.data = data
|
42
|
+
self.verbose = verbose
|
43
|
+
self.evaluations = {"fobj": [], "x": [], "cl": []}
|
44
|
+
|
45
|
+
def objective_function(self, x_values):
|
46
|
+
"""
|
47
|
+
Objective function to minimize when fitting interaction parameters.
|
48
|
+
|
49
|
+
Parameters
|
50
|
+
----------
|
51
|
+
x_values : array-like
|
52
|
+
The interaction parameters to fit.
|
53
|
+
"""
|
54
|
+
|
55
|
+
def pressure_error(Pexp, Pmodel):
|
56
|
+
return (Pexp - Pmodel) ** 2 / Pexp
|
57
|
+
|
58
|
+
def composition_error(zexp, zmodel):
|
59
|
+
return np.abs(np.log(zmodel[0] / zexp[0])) + np.abs(
|
60
|
+
np.log(zmodel[1] / zexp[1])
|
61
|
+
)
|
62
|
+
|
63
|
+
def temperature_error(Texp, Tmodel):
|
64
|
+
return (Texp - Tmodel) ** 2 / Texp
|
65
|
+
|
66
|
+
model = self._get_model(x_values, *self._get_model_args)
|
67
|
+
data = self.data
|
68
|
+
|
69
|
+
# =====================================================================
|
70
|
+
# Calculate the critical line starting from the heavy component
|
71
|
+
# ---------------------------------------------------------------------
|
72
|
+
cp_msk = data["kind"] == "critical"
|
73
|
+
if len(data[cp_msk]) > 0:
|
74
|
+
cl = model.critical_line(
|
75
|
+
z0=[0, 1],
|
76
|
+
zi=[1, 0],
|
77
|
+
a0=1e-2,
|
78
|
+
s=1e-2,
|
79
|
+
ds0=1e-3,
|
80
|
+
max_points=15000,
|
81
|
+
)
|
82
|
+
|
83
|
+
err = 0
|
84
|
+
|
85
|
+
for _, row in data.iterrows():
|
86
|
+
x = [row["x1"], 1 - row["x1"]]
|
87
|
+
y = [row["y1"], 1 - row["y1"]]
|
88
|
+
t = row["T"]
|
89
|
+
p = row["P"]
|
90
|
+
|
91
|
+
try:
|
92
|
+
w = row["weight"]
|
93
|
+
except KeyError:
|
94
|
+
w = 1
|
95
|
+
|
96
|
+
error_i = 0
|
97
|
+
|
98
|
+
# =================================================================
|
99
|
+
# Bubble point
|
100
|
+
# -----------------------------------------------------------------
|
101
|
+
if row["kind"] == "bubble":
|
102
|
+
sat = model.saturation_pressure(
|
103
|
+
x, kind="bubble", temperature=t, p0=p
|
104
|
+
)
|
105
|
+
error_i += pressure_error(p, sat["P"])
|
106
|
+
|
107
|
+
# =================================================================
|
108
|
+
# Dew points
|
109
|
+
# -----------------------------------------------------------------
|
110
|
+
if row["kind"] == "dew":
|
111
|
+
sat = model.saturation_pressure(
|
112
|
+
x, kind="dew", temperature=t, p0=p
|
113
|
+
)
|
114
|
+
|
115
|
+
error_i += pressure_error(p, sat["P"])
|
116
|
+
|
117
|
+
if row["kind"] == "PT" or row["kind"] == "liquid-liquid":
|
118
|
+
x1, y1 = solve_pt(model, row["P"], row["T"], row["kind"])
|
119
|
+
|
120
|
+
if np.isnan(x[0]):
|
121
|
+
error_i += composition_error(y, [y1, 1 - y1])
|
122
|
+
|
123
|
+
elif np.isnan(y[0]):
|
124
|
+
error_i += composition_error(x, [x1, 1 - x1])
|
125
|
+
|
126
|
+
else:
|
127
|
+
error_i += composition_error(
|
128
|
+
x, [x1, 1 - x1]
|
129
|
+
) + composition_error(y, [y1, 1 - y1])
|
130
|
+
|
131
|
+
# =================================================================
|
132
|
+
# Critical point error is calculated by finding the nearest
|
133
|
+
# critical point in the critical line to the given critical
|
134
|
+
# point in the data.
|
135
|
+
# -----------------------------------------------------------------
|
136
|
+
if row["kind"] == "critical":
|
137
|
+
cp = row
|
138
|
+
distances = (
|
139
|
+
(cp["T"] - cl["T"]) ** 2
|
140
|
+
+ ((cp["P"] - cl["P"]) ** 2)
|
141
|
+
+ ((cp["x1"] - cl["a"]) ** 2)
|
142
|
+
)
|
143
|
+
nearest = np.argmin(distances)
|
144
|
+
t_cl, p_cl, x1 = (
|
145
|
+
cl["T"][nearest],
|
146
|
+
cl["P"][nearest],
|
147
|
+
cl["a"][nearest],
|
148
|
+
)
|
149
|
+
error_i += temperature_error(cp["T"], t_cl)
|
150
|
+
error_i += pressure_error(cp["P"], p_cl)
|
151
|
+
error_i += composition_error(
|
152
|
+
[cp["x1"], 1 - cp["x1"]], [x1, 1 - x1]
|
153
|
+
)
|
154
|
+
|
155
|
+
if np.isnan(error_i):
|
156
|
+
error_i = row["P"]
|
157
|
+
err += error_i * w
|
158
|
+
|
159
|
+
# =====================================================================
|
160
|
+
# Normalize the error and save the valuation
|
161
|
+
# ---------------------------------------------------------------------
|
162
|
+
err = err / len(data)
|
163
|
+
|
164
|
+
self.evaluations["fobj"].append(err)
|
165
|
+
self.evaluations["x"].append(x_values)
|
166
|
+
|
167
|
+
if self.verbose:
|
168
|
+
print(err, x_values)
|
169
|
+
return err
|
170
|
+
|
171
|
+
def fit(self, x0, bounds, method="Nelder-Mead"):
|
172
|
+
"""Fit the model to the data.
|
173
|
+
|
174
|
+
Fit the model to the data using the objective function defined in
|
175
|
+
the objective_function method. The optimization is performed using
|
176
|
+
the scipy.optimize.minimize function.
|
177
|
+
The optimization result is stored in the `.solution` property. Which
|
178
|
+
|
179
|
+
Parameters
|
180
|
+
----------
|
181
|
+
x0 : array-like
|
182
|
+
Initial guess for the fitting parameters.
|
183
|
+
bounds : array-like
|
184
|
+
Bounds for the fitting parameters.
|
185
|
+
method : str, optional
|
186
|
+
The optimization method to use. Default is 'Nelder-Mead'.
|
187
|
+
|
188
|
+
Returns
|
189
|
+
-------
|
190
|
+
None
|
191
|
+
"""
|
192
|
+
sol = minimize(
|
193
|
+
self.objective_function, x0=x0, bounds=bounds, method=method
|
194
|
+
)
|
195
|
+
self._solution = sol
|
196
|
+
|
197
|
+
@property
|
198
|
+
def solution(self):
|
199
|
+
return self._solution
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"""Model Setters.
|
2
|
+
|
3
|
+
Compilation of functions that set a model's parameters to the values
|
4
|
+
given in the input arguments.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
|
10
|
+
def fit_kij_lij(x, model, fit_kij, fit_lij):
|
11
|
+
"""Set the kij and/or lij parameter of the model."""
|
12
|
+
mr = model.mixrule
|
13
|
+
|
14
|
+
if fit_kij and fit_lij:
|
15
|
+
kij = x[0]
|
16
|
+
lij = x[1]
|
17
|
+
mr.kij = np.array([[0, kij], [kij, 0]], order="F")
|
18
|
+
mr.lij = np.array([[0, lij], [lij, 0]], order="F")
|
19
|
+
elif fit_kij:
|
20
|
+
kij = x[0]
|
21
|
+
mr.kij = np.array([[0, kij], [kij, 0]], order="F")
|
22
|
+
elif fit_lij:
|
23
|
+
lij = x[0]
|
24
|
+
mr.lij = np.array([[0, lij], [lij, 0]], order="F")
|
25
|
+
|
26
|
+
model.set_mixrule(mr)
|
27
|
+
return model
|
28
|
+
|
29
|
+
|
30
|
+
def fit_mhv_nrtl(x, args):
|
31
|
+
"""Fit the MHV mixing rule for Cubic EoS with NRTL GE."""
|
32
|
+
from yaeos.models import NRTL, MHV
|
33
|
+
|
34
|
+
a12, a21, b12, b21, alpha = x
|
35
|
+
|
36
|
+
model = args[0]
|
37
|
+
q = args[1]
|
38
|
+
|
39
|
+
a = [
|
40
|
+
[
|
41
|
+
0,
|
42
|
+
a12,
|
43
|
+
],
|
44
|
+
[a21, 0],
|
45
|
+
]
|
46
|
+
b = [[0, b12], [b21, 0]]
|
47
|
+
c = [[0, alpha], [alpha, 0]]
|
48
|
+
|
49
|
+
nrtl = NRTL(a, b, c)
|
50
|
+
mr = MHV(ge=nrtl, q=q)
|
51
|
+
model.set_mixrule(mr)
|
52
|
+
return model
|
53
|
+
|
54
|
+
|
55
|
+
def fit_hv_nrtl(x, args):
|
56
|
+
"""Fit the HV mixing rule for Cubic EoS with NRTL GE."""
|
57
|
+
from yaeos.models import NRTL, HV
|
58
|
+
|
59
|
+
a12, a21, b12, b21, alpha = x
|
60
|
+
|
61
|
+
model = args[0]
|
62
|
+
|
63
|
+
a = [
|
64
|
+
[
|
65
|
+
0,
|
66
|
+
a12,
|
67
|
+
],
|
68
|
+
[a21, 0],
|
69
|
+
]
|
70
|
+
b = [[0, b12], [b21, 0]]
|
71
|
+
c = [[0, alpha], [alpha, 0]]
|
72
|
+
|
73
|
+
nrtl = NRTL(a, b, c)
|
74
|
+
mr = HV(nrtl)
|
75
|
+
model.set_mixrule(mr)
|
76
|
+
return model
|
yaeos/fitting/solvers.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
"""Solvers.
|
2
|
+
|
3
|
+
Module that contains different solvers for specific porpouses.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
|
9
|
+
def binary_isofugacity_x1y1pt(x, p, t, model):
|
10
|
+
"""Isofugacity evaluation at a given P and T."""
|
11
|
+
y1, x1 = x
|
12
|
+
|
13
|
+
x = np.array([x1, 1 - x1])
|
14
|
+
y = np.array([y1, 1 - y1])
|
15
|
+
|
16
|
+
lnphi_x = model.lnphi_pt(x, pressure=p, temperature=t, root="stable")
|
17
|
+
lnphi_y = model.lnphi_pt(y, pressure=p, temperature=t, root="stable")
|
18
|
+
|
19
|
+
isofug = np.log(x) + lnphi_x - (np.log(y) + lnphi_y)
|
20
|
+
|
21
|
+
return isofug**2
|
22
|
+
|
23
|
+
|
24
|
+
def solve_pt(model, p, t, kind):
|
25
|
+
"""Solve a point at a given P and T."""
|
26
|
+
try:
|
27
|
+
x10, y10 = find_init_binary_ll(model, p, t, kind)
|
28
|
+
except ValueError:
|
29
|
+
x10, y10 = 0.1, 0.9
|
30
|
+
|
31
|
+
mean = (x10 + y10) / 2
|
32
|
+
|
33
|
+
z = [mean, 1 - mean]
|
34
|
+
y0 = np.array([y10, 1 - y10])
|
35
|
+
x0 = np.array([x10, 1 - x10])
|
36
|
+
|
37
|
+
flash = model.flash_pt(z, pressure=p, temperature=t, k0=y0 / x0)
|
38
|
+
|
39
|
+
x1 = flash["x"][0]
|
40
|
+
y1 = flash["y"][0]
|
41
|
+
|
42
|
+
return x1, y1
|
43
|
+
|
44
|
+
|
45
|
+
def find_init_binary_ll(model, pressure, temperature, kind):
|
46
|
+
"""Find initial guess for a binary liquid-liquid system."""
|
47
|
+
from scipy.signal import argrelmin, argrelmax
|
48
|
+
|
49
|
+
(
|
50
|
+
p,
|
51
|
+
t,
|
52
|
+
) = (
|
53
|
+
pressure,
|
54
|
+
temperature,
|
55
|
+
)
|
56
|
+
|
57
|
+
if kind == "liquid-liquid":
|
58
|
+
root = "liquid"
|
59
|
+
else:
|
60
|
+
root = "stable"
|
61
|
+
|
62
|
+
zs = np.linspace(1e-15, 1 - 1e-15, 100)
|
63
|
+
|
64
|
+
phis = np.array(
|
65
|
+
[
|
66
|
+
model.lnphi_pt([z, 1 - z], temperature=t, pressure=p, root=root)
|
67
|
+
for z in zs
|
68
|
+
]
|
69
|
+
)
|
70
|
+
phis = np.exp(phis)
|
71
|
+
fug_1 = zs * phis[:, 0] * p
|
72
|
+
|
73
|
+
argmin = argrelmin(zs * phis[:, 0] * p)[-1] + 1
|
74
|
+
argmax = argrelmax(zs * phis[:, 0] * p)[0] - 1
|
75
|
+
|
76
|
+
fug = np.mean([fug_1[argmin], fug_1[argmax]])
|
77
|
+
|
78
|
+
if fug > fug_1[-1]:
|
79
|
+
fug = np.mean([fug_1[argmin[0]], fug_1[-1]])
|
80
|
+
|
81
|
+
msk = zs < zs[argmax]
|
82
|
+
x1 = zs[msk][np.argmin(np.abs(fug - fug_1[msk]))]
|
83
|
+
|
84
|
+
msk = zs > zs[argmin]
|
85
|
+
y1 = zs[msk][np.argmin(np.abs(fug - fug_1[msk]))]
|
86
|
+
|
87
|
+
return x1, y1
|
yaeos/gpec.py
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
"""Global Phase Equilibria Calculations.
|
2
|
+
|
3
|
+
Module that implements the GPEC algorithm for calculation of GPEDs and its
|
4
|
+
derivatives to obtain isopleths, isotherms and isobars.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import matplotlib.pyplot as plt
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
|
11
|
+
from yaeos.core import ArModel
|
12
|
+
|
13
|
+
MAX_POINTS = 10000
|
14
|
+
|
15
|
+
|
16
|
+
class GPEC:
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
model: ArModel,
|
21
|
+
max_pressure=2500,
|
22
|
+
max_points=10000,
|
23
|
+
stability_analysis=True,
|
24
|
+
step_21=1e-2,
|
25
|
+
step_12=1e-5,
|
26
|
+
):
|
27
|
+
self._z0 = np.array([0, 1])
|
28
|
+
self._zi = np.array([1, 0])
|
29
|
+
self._model = model
|
30
|
+
|
31
|
+
# Calculate the pure saturation pressures of each component and
|
32
|
+
# save it as an internal variable
|
33
|
+
psats = [model.pure_saturation_pressures(i) for i in [1, 2]]
|
34
|
+
self._pures = psats
|
35
|
+
|
36
|
+
# Calculate the critical line starting from the almost pure second
|
37
|
+
# component.
|
38
|
+
diff = 1e-3
|
39
|
+
cl, cep = model.critical_line(
|
40
|
+
z0=self._z0,
|
41
|
+
zi=self._zi,
|
42
|
+
ns=1,
|
43
|
+
s=diff,
|
44
|
+
a0=diff,
|
45
|
+
ds0=step_21,
|
46
|
+
stop_pressure=max_pressure,
|
47
|
+
max_points=max_points,
|
48
|
+
stability_analysis=stability_analysis,
|
49
|
+
)
|
50
|
+
|
51
|
+
self._cl21 = cl
|
52
|
+
self._cep21 = cep
|
53
|
+
|
54
|
+
# Check if the critical line did not reach to the pure first component.
|
55
|
+
# if not, calculate the critical line starting from the almost pure
|
56
|
+
# first component. It is important to make small steps because this
|
57
|
+
# kind of line can be pretty short.
|
58
|
+
if not np.isnan(cep["T"]) or (
|
59
|
+
abs(cl["T"][-1] - psats[0]["T"][-1]) > 10
|
60
|
+
and abs(cl["P"][-1] - psats[0]["P"][-1] > 10)
|
61
|
+
):
|
62
|
+
cl, cep = model.critical_line(
|
63
|
+
z0=self._z0,
|
64
|
+
zi=self._zi,
|
65
|
+
ns=1,
|
66
|
+
s=1 - diff / 10,
|
67
|
+
a0=1 - diff / 10,
|
68
|
+
ds0=-step_12,
|
69
|
+
stop_pressure=max_pressure,
|
70
|
+
max_points=max_points,
|
71
|
+
stability_analysis=stability_analysis,
|
72
|
+
)
|
73
|
+
|
74
|
+
self._cl12 = cl
|
75
|
+
self._cep12 = cep
|
76
|
+
else:
|
77
|
+
self._cl12 = None
|
78
|
+
self._cep12 = None
|
79
|
+
|
80
|
+
a, T, V = model.critical_line_liquid_liquid(
|
81
|
+
z0=self._z0, zi=self._zi, pressure=max_pressure, t0=500
|
82
|
+
)
|
83
|
+
|
84
|
+
cl, cep = model.critical_line(
|
85
|
+
z0=self._z0,
|
86
|
+
zi=self._zi,
|
87
|
+
ns=4,
|
88
|
+
s=np.log(max_pressure),
|
89
|
+
a0=a,
|
90
|
+
v0=V,
|
91
|
+
t0=T,
|
92
|
+
p0=max_pressure,
|
93
|
+
ds0=-1e-1,
|
94
|
+
stop_pressure=max_pressure * 1.1,
|
95
|
+
max_points=max_points,
|
96
|
+
stability_analysis=stability_analysis,
|
97
|
+
)
|
98
|
+
|
99
|
+
self._cl_ll = cl
|
100
|
+
self._cep_ll = cep
|
101
|
+
|
102
|
+
def plot_gped(self):
|
103
|
+
for pure in self._pures:
|
104
|
+
plt.plot(pure["T"], pure["P"], color="green")
|
105
|
+
|
106
|
+
plt.plot(self._cl21["T"], self._cl21["P"], color="black")
|
107
|
+
if self._cl12:
|
108
|
+
plt.plot(self._cl12["T"], self._cl12["P"], color="black")
|
109
|
+
if self._cl_ll:
|
110
|
+
plt.plot(self._cl_ll["T"], self._cl_ll["P"], color="black")
|
111
|
+
|
112
|
+
plt.xlabel("Temperature (K)")
|
113
|
+
plt.ylabel("Pressure (bar)")
|
114
|
+
|
115
|
+
def plot_pxy(self, temperature, a0=1e-5):
|
116
|
+
"""Plot a Pxy phase diagram"""
|
117
|
+
psat_1, psat_2 = self._pures
|
118
|
+
|
119
|
+
px_12 = px_21 = px_iso = None
|
120
|
+
|
121
|
+
# Below saturation temperature of light component
|
122
|
+
loc = np.argmin(abs(psat_2["T"] - temperature))
|
123
|
+
p0 = psat_2["P"][loc]
|
124
|
+
px_21 = self._model.phase_envelope_px(
|
125
|
+
self._z0,
|
126
|
+
self._zi,
|
127
|
+
temperature=temperature,
|
128
|
+
kind="bubble",
|
129
|
+
p0=p0,
|
130
|
+
a0=a0,
|
131
|
+
ds0=1e-3,
|
132
|
+
max_points=MAX_POINTS,
|
133
|
+
)
|
134
|
+
|
135
|
+
pxs = [px_12, px_21, px_iso]
|
136
|
+
|
137
|
+
for px in pxs:
|
138
|
+
if px:
|
139
|
+
plt.plot(px.main_phases_compositions[:, 0, 0], px["P"])
|
140
|
+
plt.plot(px.reference_phase_compositions[:, 0], px["P"])
|
141
|
+
|
142
|
+
plt.xlabel(r"$x_1$, $y_1$")
|
143
|
+
plt.ylabel("Pressure (bar)")
|
144
|
+
|
145
|
+
return pxs
|
146
|
+
|
147
|
+
def plot_txy(self, pressure, a0=1e-5):
|
148
|
+
"""Plot a Txy phase diagram"""
|
149
|
+
psat_1, psat_2 = self._pures
|
150
|
+
|
151
|
+
tx_12 = tx_21 = tx_ll = None
|
152
|
+
|
153
|
+
# Below critical pressure of heavy component
|
154
|
+
if pressure < psat_2["P"][-1]:
|
155
|
+
loc = np.argmin(abs(psat_2["P"] - pressure))
|
156
|
+
t0 = psat_2["T"][loc]
|
157
|
+
|
158
|
+
tx_21 = self._model.phase_envelope_tx(
|
159
|
+
self._z0,
|
160
|
+
self._zi,
|
161
|
+
pressure=pressure,
|
162
|
+
kind="dew",
|
163
|
+
t0=t0,
|
164
|
+
a0=a0,
|
165
|
+
ds0=1e-5,
|
166
|
+
max_points=MAX_POINTS,
|
167
|
+
)
|
168
|
+
if pressure < psat_1["P"][-1]:
|
169
|
+
# Below critical pressure of the light component
|
170
|
+
loc = np.argmin(abs(psat_1["P"] - pressure))
|
171
|
+
t0 = psat_1["T"][loc]
|
172
|
+
|
173
|
+
tx_12 = self._model.phase_envelope_tx(
|
174
|
+
self._z0,
|
175
|
+
self._zi,
|
176
|
+
pressure=pressure,
|
177
|
+
kind="bubble",
|
178
|
+
t0=t0,
|
179
|
+
a0=1 - a0,
|
180
|
+
ds0=-1e-5,
|
181
|
+
max_points=MAX_POINTS,
|
182
|
+
)
|
183
|
+
|
184
|
+
if self._cl_ll:
|
185
|
+
loc = np.argmin(abs(self._cl_ll["P"] - pressure))
|
186
|
+
t0, p = self._cl_ll["T"][loc], self._cl_ll["P"][loc]
|
187
|
+
if abs(p - pressure) < 1 or pressure > psat_1["P"][-1]:
|
188
|
+
|
189
|
+
a = self._cl_ll["a"][loc]
|
190
|
+
z = a * self._zi + (1 - a) * self._z0
|
191
|
+
x_l0 = [z.copy()]
|
192
|
+
|
193
|
+
x_l0[0][0] += 1e-5
|
194
|
+
x_l0[0][1] -= 1e-5
|
195
|
+
w0 = z.copy()
|
196
|
+
w0[0] -= 1e-5
|
197
|
+
w0[1] += 1e-5
|
198
|
+
|
199
|
+
tx_ll = self._model.phase_envelope_tx_mp(
|
200
|
+
z0=self._z0,
|
201
|
+
zi=self._zi,
|
202
|
+
p=pressure,
|
203
|
+
kinds_x=["liquid"],
|
204
|
+
kind_w="liquid",
|
205
|
+
x_l0=x_l0,
|
206
|
+
w0=w0,
|
207
|
+
betas0=[1],
|
208
|
+
t0=t0,
|
209
|
+
alpha0=a + 1e-5,
|
210
|
+
ns0=len(z) + 3,
|
211
|
+
ds0=1e-4,
|
212
|
+
max_points=MAX_POINTS,
|
213
|
+
)
|
214
|
+
|
215
|
+
txs = [tx_12, tx_21, tx_ll]
|
216
|
+
|
217
|
+
for tx in txs:
|
218
|
+
if tx:
|
219
|
+
plt.plot(tx.main_phases_compositions[:, 0, 0], tx["T"])
|
220
|
+
plt.plot(tx.reference_phase_compositions[:, 0], tx["T"])
|
221
|
+
|
222
|
+
plt.xlabel(r"$x_1$, $y_1$")
|
223
|
+
plt.ylabel("Temperature (K)")
|
224
|
+
|
225
|
+
return txs
|
yaeos/lib/__init__.py
ADDED
Binary file
|
yaeos/models/__init__.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
"""Models module.
|
2
|
+
|
3
|
+
Yaeos models module. This module provides the following submodules:
|
4
|
+
|
5
|
+
- excess_gibbs: Excess Gibbs energy models
|
6
|
+
- NRTL: non-random two-liquid model
|
7
|
+
- UNIFACVLE: Original UNIFAC VLE model
|
8
|
+
- UNIQUAC: UNIversal QUAsiChemical Excess Gibbs free energy model
|
9
|
+
|
10
|
+
- residual_helmholtz: Residual Helmholtz energy models
|
11
|
+
- Cubic EoS:
|
12
|
+
- PengRobinson76: Peng-Robinson model (1976)
|
13
|
+
- PengRobinson78: Peng-Robinson model (1978)
|
14
|
+
- SoaveRedlichKwong: Soave-Redlich-Kwong model
|
15
|
+
- RKPR: RKPR model
|
16
|
+
- Mixing rules: mixing rules for cubic EoS
|
17
|
+
- QMR: cuadratic mixing rule
|
18
|
+
- MHV: modified Huron-Vidal mixing rule
|
19
|
+
- Multifluid EoS:
|
20
|
+
- GERG2008: GERG2008 Residual contribution
|
21
|
+
"""
|
22
|
+
|
23
|
+
from . import excess_gibbs, residual_helmholtz
|
24
|
+
|
25
|
+
|
26
|
+
__all__ = ["excess_gibbs", "residual_helmholtz"]
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""Gibbs Excess Models module.
|
2
|
+
|
3
|
+
Yaeos Gibbs excess module. This module provides the following submodules:
|
4
|
+
|
5
|
+
- excess_gibbs: Excess Gibbs energy models
|
6
|
+
- NRTL: non-random two-liquid model
|
7
|
+
- UNIFACVLE: Original UNIFAC VLE model
|
8
|
+
- UNIFACPSRK: UNIFAC-PSRK model (Predictive Soave-Redlich-Kwong)
|
9
|
+
- UNIFACDortmund: UNIFAC Dortmund model
|
10
|
+
- UNIQUAC: UNIversal QUAsiChemical Excess Gibbs free energy model
|
11
|
+
"""
|
12
|
+
|
13
|
+
from .dortmund import UNIFACDortmund
|
14
|
+
from .nrtl import NRTL
|
15
|
+
from .psrk_unifac import UNIFACPSRK
|
16
|
+
from .unifac import UNIFACVLE
|
17
|
+
from .uniquac import UNIQUAC
|
18
|
+
|
19
|
+
|
20
|
+
__all__ = ["NRTL", "UNIFACVLE", "UNIFACPSRK", "UNIFACDortmund", "UNIQUAC"]
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""UNIFAC Dortmund Module."""
|
2
|
+
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from yaeos.core import GeModel
|
6
|
+
from yaeos.lib import yaeos_c
|
7
|
+
from yaeos.models.groups import groups_from_dicts
|
8
|
+
|
9
|
+
|
10
|
+
class UNIFACDortmund(GeModel):
|
11
|
+
"""UNIFAC Dortmund model.
|
12
|
+
|
13
|
+
Please refer to the `yaeos` user documentation for an in-depth look at the
|
14
|
+
model's information: https://ipqa-research.github.io/yaeos/page/index.html
|
15
|
+
|
16
|
+
Parameters
|
17
|
+
----------
|
18
|
+
molecules : list of dict
|
19
|
+
List of dicts with the groups and their amounts for each molecule.
|
20
|
+
|
21
|
+
Example
|
22
|
+
-------
|
23
|
+
.. code-block:: python
|
24
|
+
|
25
|
+
from yaeos import UNIFACDortmund
|
26
|
+
|
27
|
+
# Groups for water and ethanol
|
28
|
+
water = {16: 1}
|
29
|
+
ethanol = {1: 1, 2: 1, 14: 1}
|
30
|
+
|
31
|
+
groups = [water, ethanol]
|
32
|
+
|
33
|
+
model = UNIFACDortmund(groups)
|
34
|
+
|
35
|
+
model.ln_gamma([0.5, 0.5], 298.15)
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, molecules: List[dict]) -> None:
|
39
|
+
|
40
|
+
(number_of_groups, groups_ids, groups_ammounts) = groups_from_dicts(
|
41
|
+
molecules
|
42
|
+
)
|
43
|
+
self.id = yaeos_c.unifac_dortmund(
|
44
|
+
ngs=number_of_groups, g_ids=groups_ids, g_v=groups_ammounts
|
45
|
+
)
|