yaeos 3.1.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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 +54 -0
- yaeos/constants.py +4 -0
- yaeos/core.py +2473 -0
- yaeos/envelopes.py +407 -0
- yaeos/fitting/__init__.py +12 -0
- yaeos/fitting/core.py +193 -0
- yaeos/fitting/model_setters.py +76 -0
- yaeos/fitting/solvers.py +87 -0
- yaeos/gpec.py +104 -0
- yaeos/lib/__init__.py +9 -0
- yaeos/lib/yaeos_python.cpython-313-x86_64-linux-gnu.so +0 -0
- yaeos/models/__init__.py +24 -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-3.1.0.dist-info/METADATA +87 -0
- yaeos-3.1.0.dist-info/RECORD +32 -0
- yaeos-3.1.0.dist-info/WHEEL +6 -0
- yaeos.libs/libblas-357956a1.so.3.4.2 +0 -0
- yaeos.libs/libgfortran-040039e1.so.5.0.0 +0 -0
- yaeos.libs/libgfortran-91cc3cb1.so.3.0.0 +0 -0
- yaeos.libs/libgomp-a34b3233.so.1.0.0 +0 -0
- yaeos.libs/liblapack-1ad85175.so.3.4.2 +0 -0
- yaeos.libs/libquadmath-96973f99.so.0.0.0 +0 -0
yaeos/envelopes.py
ADDED
@@ -0,0 +1,407 @@
|
|
1
|
+
"""Envelopes.
|
2
|
+
|
3
|
+
This module contains the classes that wrapp the data structures used to
|
4
|
+
represent different kinds of phase envelopes.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from IPython.display import display
|
8
|
+
|
9
|
+
import matplotlib.pyplot as plt
|
10
|
+
|
11
|
+
import numpy as np
|
12
|
+
|
13
|
+
import pandas as pd
|
14
|
+
|
15
|
+
|
16
|
+
class PTEnvelope:
|
17
|
+
"""PTEnvelope.
|
18
|
+
|
19
|
+
This class represents a pressure-temperature envelope.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
global_composition,
|
25
|
+
main_phases_compositions,
|
26
|
+
reference_phase_compositions,
|
27
|
+
main_phases_molar_fractions,
|
28
|
+
pressures,
|
29
|
+
temperatures,
|
30
|
+
iterations,
|
31
|
+
specified_variable,
|
32
|
+
):
|
33
|
+
|
34
|
+
msk = ~np.isnan(pressures)
|
35
|
+
self.number_of_components = len(global_composition)
|
36
|
+
self.number_of_phases = main_phases_compositions.shape[1]
|
37
|
+
self.global_composition = global_composition
|
38
|
+
self.main_phases_compositions = main_phases_compositions[msk, :, :]
|
39
|
+
self.reference_phase_compositions = reference_phase_compositions[
|
40
|
+
msk, :
|
41
|
+
]
|
42
|
+
self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
|
43
|
+
self.pressures = pressures[msk]
|
44
|
+
self.temperatures = temperatures[msk]
|
45
|
+
self.iterations = iterations[msk]
|
46
|
+
self.specified_variable = specified_variable[msk]
|
47
|
+
|
48
|
+
df = pd.DataFrame()
|
49
|
+
|
50
|
+
df["T"] = self.temperatures
|
51
|
+
df["P"] = self.pressures
|
52
|
+
|
53
|
+
for i in range(self.number_of_components):
|
54
|
+
for j in range(self.number_of_phases):
|
55
|
+
df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
|
56
|
+
df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
|
57
|
+
|
58
|
+
for i in range(self.number_of_phases):
|
59
|
+
df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
|
60
|
+
|
61
|
+
self.df = df
|
62
|
+
|
63
|
+
idx = []
|
64
|
+
for phase in range(self.number_of_phases):
|
65
|
+
cp_idx = []
|
66
|
+
lnK = np.log(
|
67
|
+
self.reference_phase_compositions
|
68
|
+
/ self.main_phases_compositions[:, phase]
|
69
|
+
)
|
70
|
+
|
71
|
+
for i in range(1, len(lnK)):
|
72
|
+
if all(lnK[i, :] * lnK[i - 1, :] < 0):
|
73
|
+
cp_idx.append(i)
|
74
|
+
|
75
|
+
idx.append(cp_idx)
|
76
|
+
|
77
|
+
self.cp = idx
|
78
|
+
|
79
|
+
# lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
|
80
|
+
# lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
|
81
|
+
# for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
|
82
|
+
# lnK2 = np.log(c["w"]/c["x_l"][i, 1])
|
83
|
+
# crit = (lnK * lnK2 < 0).all()
|
84
|
+
# if crit:
|
85
|
+
# print("asd")
|
86
|
+
|
87
|
+
def __getitem__(self, key):
|
88
|
+
if "key" in self.__dict__:
|
89
|
+
return self.__dict__["key"]
|
90
|
+
elif isinstance(key, np.ndarray):
|
91
|
+
return PTEnvelope(
|
92
|
+
global_composition=self.global_composition,
|
93
|
+
main_phases_compositions=self.main_phases_compositions[key],
|
94
|
+
reference_phase_compositions=self.reference_phase_compositions[
|
95
|
+
key
|
96
|
+
],
|
97
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions[
|
98
|
+
key
|
99
|
+
],
|
100
|
+
pressures=self.pressures[key],
|
101
|
+
temperatures=self.temperatures[key],
|
102
|
+
iterations=self.iterations[key],
|
103
|
+
specified_variable=self.specified_variable[key],
|
104
|
+
)
|
105
|
+
elif key == "T":
|
106
|
+
return self.temperatures
|
107
|
+
elif key == "Tc":
|
108
|
+
return self.temperatures[self.cp]
|
109
|
+
elif key == "Pc":
|
110
|
+
return self.pressures[self.cp]
|
111
|
+
elif key == "P":
|
112
|
+
return self.pressures
|
113
|
+
elif key == "z":
|
114
|
+
return self.global_composition
|
115
|
+
elif key == "x":
|
116
|
+
return self.main_phases_compositions
|
117
|
+
elif key == "w":
|
118
|
+
return self.reference_phase_compositions
|
119
|
+
|
120
|
+
def plot(self):
|
121
|
+
plt.plot(self.temperatures, self.pressures)
|
122
|
+
plt.xlabel("Temperature (K)")
|
123
|
+
plt.ylabel("Pressure [bar]")
|
124
|
+
plt.title("PT Envelope")
|
125
|
+
for cp in self.cp:
|
126
|
+
plt.scatter(
|
127
|
+
self.temperatures[cp], self.pressures[cp], color="black"
|
128
|
+
)
|
129
|
+
|
130
|
+
def __repr__(self):
|
131
|
+
display(self.df)
|
132
|
+
return ""
|
133
|
+
|
134
|
+
def __mul__(self, other):
|
135
|
+
return PTEnvelope(
|
136
|
+
global_composition=self.global_composition,
|
137
|
+
main_phases_compositions=self.main_phases_compositions * other,
|
138
|
+
reference_phase_compositions=self.reference_phase_compositions
|
139
|
+
* other,
|
140
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions
|
141
|
+
* other,
|
142
|
+
pressures=self.pressures * other,
|
143
|
+
temperatures=self.temperatures * other,
|
144
|
+
iterations=self.iterations,
|
145
|
+
specified_variable=self.specified_variable,
|
146
|
+
)
|
147
|
+
|
148
|
+
def __len__(self):
|
149
|
+
return len(self.temperatures)
|
150
|
+
|
151
|
+
|
152
|
+
class PXEnvelope:
|
153
|
+
"""PXEnvelope.
|
154
|
+
|
155
|
+
This class represents a pressure-composition envelope.
|
156
|
+
"""
|
157
|
+
|
158
|
+
def __init__(
|
159
|
+
self,
|
160
|
+
temperature,
|
161
|
+
global_composition_0,
|
162
|
+
global_composition_i,
|
163
|
+
main_phases_compositions,
|
164
|
+
reference_phase_compositions,
|
165
|
+
main_phases_molar_fractions,
|
166
|
+
pressures,
|
167
|
+
alphas,
|
168
|
+
iterations,
|
169
|
+
specified_variable,
|
170
|
+
):
|
171
|
+
|
172
|
+
msk = ~np.isnan(pressures)
|
173
|
+
self.temperature = temperature
|
174
|
+
self.number_of_components = len(global_composition_0)
|
175
|
+
self.number_of_phases = main_phases_compositions.shape[1]
|
176
|
+
self.global_composition_0 = global_composition_0
|
177
|
+
self.global_composition_i = global_composition_i
|
178
|
+
self.main_phases_compositions = main_phases_compositions[msk, :, :]
|
179
|
+
self.reference_phase_compositions = reference_phase_compositions[
|
180
|
+
msk, :
|
181
|
+
]
|
182
|
+
self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
|
183
|
+
self.pressures = pressures[msk]
|
184
|
+
self.alphas = alphas[msk]
|
185
|
+
self.iterations = iterations[msk]
|
186
|
+
self.specified_variable = specified_variable[msk]
|
187
|
+
|
188
|
+
df = pd.DataFrame()
|
189
|
+
|
190
|
+
df["alpha"] = self.alphas
|
191
|
+
df["P"] = self.pressures
|
192
|
+
|
193
|
+
for i in range(self.number_of_components):
|
194
|
+
for j in range(self.number_of_phases):
|
195
|
+
df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
|
196
|
+
df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
|
197
|
+
|
198
|
+
for i in range(self.number_of_phases):
|
199
|
+
df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
|
200
|
+
|
201
|
+
self.df = df
|
202
|
+
|
203
|
+
idx = []
|
204
|
+
for phase in range(self.number_of_phases):
|
205
|
+
cp_idx = []
|
206
|
+
lnK = np.log(
|
207
|
+
self.reference_phase_compositions
|
208
|
+
/ self.main_phases_compositions[:, phase]
|
209
|
+
)
|
210
|
+
|
211
|
+
for i in range(1, len(lnK)):
|
212
|
+
if all(lnK[i, :] * lnK[i - 1, :] < 0):
|
213
|
+
cp_idx.append(i)
|
214
|
+
|
215
|
+
idx.append(cp_idx)
|
216
|
+
|
217
|
+
self.cp = idx
|
218
|
+
|
219
|
+
# lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
|
220
|
+
# lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
|
221
|
+
# for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
|
222
|
+
# lnK2 = np.log(c["w"]/c["x_l"][i, 1])
|
223
|
+
# crit = (lnK * lnK2 < 0).all()
|
224
|
+
# if crit:
|
225
|
+
# print("asd")
|
226
|
+
|
227
|
+
def __getitem__(self, key):
|
228
|
+
if "key" in self.__dict__:
|
229
|
+
return self.__dict__["key"]
|
230
|
+
elif isinstance(key, np.ndarray):
|
231
|
+
return PXEnvelope(
|
232
|
+
global_composition_0=self.global_composition_0,
|
233
|
+
global_composition_i=self.global_composition_i,
|
234
|
+
temperature=self.temperature,
|
235
|
+
main_phases_compositions=self.main_phases_compositions[key],
|
236
|
+
reference_phase_compositions=self.reference_phase_compositions[
|
237
|
+
key
|
238
|
+
],
|
239
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions[
|
240
|
+
key
|
241
|
+
],
|
242
|
+
pressures=self.pressures[key],
|
243
|
+
alphas=self.alphas[key],
|
244
|
+
iterations=self.iterations[key],
|
245
|
+
specified_variable=self.specified_variable[key],
|
246
|
+
)
|
247
|
+
elif key == "alpha" or key == "a":
|
248
|
+
return self.alphas
|
249
|
+
elif key == "P":
|
250
|
+
return self.pressures
|
251
|
+
elif key == "z":
|
252
|
+
return self.global_composition
|
253
|
+
elif key == "x":
|
254
|
+
return self.main_phases_compositions
|
255
|
+
elif key == "w":
|
256
|
+
return self.reference_phase_compositions
|
257
|
+
|
258
|
+
def __repr__(self):
|
259
|
+
return self.df.__repr__()
|
260
|
+
|
261
|
+
def __mul__(self, other):
|
262
|
+
return PXEnvelope(
|
263
|
+
global_composition_0=self.global_composition_0,
|
264
|
+
global_composition_i=self.global_composition_i,
|
265
|
+
temperature=self.temperature,
|
266
|
+
main_phases_compositions=self.main_phases_compositions * other,
|
267
|
+
reference_phase_compositions=self.reference_phase_compositions
|
268
|
+
* other,
|
269
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions
|
270
|
+
* other,
|
271
|
+
pressures=self.pressures * other,
|
272
|
+
alphas=self.alphas * other,
|
273
|
+
iterations=self.iterations,
|
274
|
+
specified_variable=self.specified_variable,
|
275
|
+
)
|
276
|
+
|
277
|
+
def __len__(self):
|
278
|
+
return len(self.alphas)
|
279
|
+
|
280
|
+
|
281
|
+
class TXEnvelope:
|
282
|
+
"""TXEnvelope.
|
283
|
+
|
284
|
+
This class represents a pressure-composition envelope.
|
285
|
+
"""
|
286
|
+
|
287
|
+
def __init__(
|
288
|
+
self,
|
289
|
+
pressure,
|
290
|
+
global_composition_0,
|
291
|
+
global_composition_i,
|
292
|
+
main_phases_compositions,
|
293
|
+
reference_phase_compositions,
|
294
|
+
main_phases_molar_fractions,
|
295
|
+
temperatures,
|
296
|
+
alphas,
|
297
|
+
iterations,
|
298
|
+
specified_variable,
|
299
|
+
):
|
300
|
+
|
301
|
+
msk = ~np.isnan(temperatures)
|
302
|
+
self.temperature = pressure
|
303
|
+
self.number_of_components = len(global_composition_0)
|
304
|
+
self.number_of_phases = main_phases_compositions.shape[1]
|
305
|
+
self.global_composition_0 = global_composition_0
|
306
|
+
self.global_composition_i = global_composition_i
|
307
|
+
self.main_phases_compositions = main_phases_compositions[msk, :, :]
|
308
|
+
self.reference_phase_compositions = reference_phase_compositions[
|
309
|
+
msk, :
|
310
|
+
]
|
311
|
+
self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
|
312
|
+
self.temperatures = temperatures[msk]
|
313
|
+
self.alphas = alphas[msk]
|
314
|
+
self.iterations = iterations[msk]
|
315
|
+
self.specified_variable = specified_variable[msk]
|
316
|
+
|
317
|
+
df = pd.DataFrame()
|
318
|
+
|
319
|
+
df["alpha"] = self.alphas
|
320
|
+
df["T"] = self.temperatures
|
321
|
+
|
322
|
+
for i in range(self.number_of_components):
|
323
|
+
for j in range(self.number_of_phases):
|
324
|
+
df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
|
325
|
+
df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
|
326
|
+
|
327
|
+
for i in range(self.number_of_phases):
|
328
|
+
df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
|
329
|
+
|
330
|
+
self.df = df
|
331
|
+
|
332
|
+
idx = []
|
333
|
+
for phase in range(self.number_of_phases):
|
334
|
+
cp_idx = []
|
335
|
+
lnK = np.log(
|
336
|
+
self.reference_phase_compositions
|
337
|
+
/ self.main_phases_compositions[:, phase]
|
338
|
+
)
|
339
|
+
|
340
|
+
for i in range(1, len(lnK)):
|
341
|
+
if all(lnK[i, :] * lnK[i - 1, :] < 0):
|
342
|
+
cp_idx.append(i)
|
343
|
+
|
344
|
+
idx.append(cp_idx)
|
345
|
+
|
346
|
+
self.cp = idx
|
347
|
+
|
348
|
+
# lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
|
349
|
+
# lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
|
350
|
+
# for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
|
351
|
+
# lnK2 = np.log(c["w"]/c["x_l"][i, 1])
|
352
|
+
# crit = (lnK * lnK2 < 0).all()
|
353
|
+
# if crit:
|
354
|
+
# print("asd")
|
355
|
+
|
356
|
+
def __getitem__(self, key):
|
357
|
+
if "key" in self.__dict__:
|
358
|
+
return self.__dict__["key"]
|
359
|
+
elif isinstance(key, np.ndarray):
|
360
|
+
return TXEnvelope(
|
361
|
+
global_composition_0=self.global_composition_0,
|
362
|
+
global_composition_i=self.global_composition_i,
|
363
|
+
pressure=self.pressure,
|
364
|
+
main_phases_compositions=self.main_phases_compositions[key],
|
365
|
+
reference_phase_compositions=self.reference_phase_compositions[
|
366
|
+
key
|
367
|
+
],
|
368
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions[
|
369
|
+
key
|
370
|
+
],
|
371
|
+
temperatures=self.temperatures[key],
|
372
|
+
alphas=self.alphas[key],
|
373
|
+
iterations=self.iterations[key],
|
374
|
+
specified_variable=self.specified_variable[key],
|
375
|
+
)
|
376
|
+
elif key == "alpha" or key == "a":
|
377
|
+
return self.alphas
|
378
|
+
elif key == "T":
|
379
|
+
return self.temperatures
|
380
|
+
elif key == "z":
|
381
|
+
return self.global_composition
|
382
|
+
elif key == "x":
|
383
|
+
return self.main_phases_compositions
|
384
|
+
elif key == "w":
|
385
|
+
return self.reference_phase_compositions
|
386
|
+
|
387
|
+
def __repr__(self):
|
388
|
+
return self.df.__repr__()
|
389
|
+
|
390
|
+
def __mul__(self, other):
|
391
|
+
return TXEnvelope(
|
392
|
+
global_composition_0=self.global_composition_0,
|
393
|
+
global_composition_i=self.global_composition_i,
|
394
|
+
pressure=self.pressure,
|
395
|
+
main_phases_compositions=self.main_phases_compositions * other,
|
396
|
+
reference_phase_compositions=self.reference_phase_compositions
|
397
|
+
* other,
|
398
|
+
main_phases_molar_fractions=self.main_phases_molar_fractions
|
399
|
+
* other,
|
400
|
+
temperatures=self.temperature * other,
|
401
|
+
alphas=self.alphas * other,
|
402
|
+
iterations=self.iterations,
|
403
|
+
specified_variable=self.specified_variable,
|
404
|
+
)
|
405
|
+
|
406
|
+
def __len__(self):
|
407
|
+
return len(self.alphas)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""yaeos fitting module.
|
2
|
+
|
3
|
+
This module provides classes and functions for fitting binary interaction
|
4
|
+
parameters to experimental data.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from yaeos.fitting.core import BinaryFitter
|
8
|
+
from yaeos.fitting.model_setters import fit_kij_lij
|
9
|
+
from yaeos.fitting.solvers import solve_pt
|
10
|
+
|
11
|
+
|
12
|
+
__all__ = ["BinaryFitter", "fit_kij_lij", "solve_pt"]
|
yaeos/fitting/core.py
ADDED
@@ -0,0 +1,193 @@
|
|
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
|
+
cl = model.critical_line(
|
73
|
+
z0=[0, 1], zi=[1, 0], a0=1e-2, s=1e-2, ds0=1e-3, max_points=5000
|
74
|
+
)
|
75
|
+
|
76
|
+
err = 0
|
77
|
+
|
78
|
+
for _, row in data.iterrows():
|
79
|
+
x = [row["x1"], 1 - row["x1"]]
|
80
|
+
y = [row["y1"], 1 - row["y1"]]
|
81
|
+
t = row["T"]
|
82
|
+
p = row["P"]
|
83
|
+
|
84
|
+
try:
|
85
|
+
w = row["weight"]
|
86
|
+
except KeyError:
|
87
|
+
w = 1
|
88
|
+
|
89
|
+
error_i = 0
|
90
|
+
|
91
|
+
# =================================================================
|
92
|
+
# Bubble point
|
93
|
+
# -----------------------------------------------------------------
|
94
|
+
if row["kind"] == "bubble":
|
95
|
+
sat = model.saturation_pressure(
|
96
|
+
x, kind="bubble", temperature=t, p0=p
|
97
|
+
)
|
98
|
+
error_i += pressure_error(p, sat["P"])
|
99
|
+
|
100
|
+
# =================================================================
|
101
|
+
# Dew points
|
102
|
+
# -----------------------------------------------------------------
|
103
|
+
if row["kind"] == "dew":
|
104
|
+
sat = model.saturation_pressure(
|
105
|
+
x, kind="dew", temperature=t, p0=p
|
106
|
+
)
|
107
|
+
|
108
|
+
error_i += pressure_error(p, sat["P"])
|
109
|
+
|
110
|
+
if row["kind"] == "PT" or row["kind"] == "liquid-liquid":
|
111
|
+
x1, y1 = solve_pt(model, row["P"], row["T"], row["kind"])
|
112
|
+
|
113
|
+
if np.isnan(x[0]):
|
114
|
+
error_i += composition_error(y, [y1, 1 - y1])
|
115
|
+
|
116
|
+
elif np.isnan(y[0]):
|
117
|
+
error_i += composition_error(x, [x1, 1 - x1])
|
118
|
+
|
119
|
+
else:
|
120
|
+
error_i += composition_error(
|
121
|
+
x, [x1, 1 - x1]
|
122
|
+
) + composition_error(y, [y1, 1 - y1])
|
123
|
+
|
124
|
+
# =================================================================
|
125
|
+
# Critical point error is calculated by finding the nearest
|
126
|
+
# critical point in the critical line to the given critical
|
127
|
+
# point in the data.
|
128
|
+
# -----------------------------------------------------------------
|
129
|
+
if row["kind"] == "critical":
|
130
|
+
cp = row
|
131
|
+
distances = (
|
132
|
+
(cp["T"] - cl["T"]) ** 2
|
133
|
+
+ ((cp["P"] - cl["P"]) ** 2)
|
134
|
+
+ ((cp["x1"] - cl["a"]) ** 2)
|
135
|
+
)
|
136
|
+
nearest = np.argmin(distances)
|
137
|
+
t_cl, p_cl, x1 = (
|
138
|
+
cl["T"][nearest],
|
139
|
+
cl["P"][nearest],
|
140
|
+
cl["a"][nearest],
|
141
|
+
)
|
142
|
+
error_i += temperature_error(cp["T"], t_cl)
|
143
|
+
error_i += pressure_error(cp["P"], p_cl)
|
144
|
+
error_i += composition_error(
|
145
|
+
[cp["x1"], 1 - cp["x1"]], [x1, 1 - x1]
|
146
|
+
)
|
147
|
+
|
148
|
+
if np.isnan(error_i):
|
149
|
+
error_i = row["P"]
|
150
|
+
err += error_i * w
|
151
|
+
|
152
|
+
# =====================================================================
|
153
|
+
# Normalize the error and save the valuation
|
154
|
+
# ---------------------------------------------------------------------
|
155
|
+
err = err / len(data)
|
156
|
+
|
157
|
+
self.evaluations["fobj"].append(err)
|
158
|
+
self.evaluations["x"].append(x_values)
|
159
|
+
self.evaluations["cl"].append(cl)
|
160
|
+
|
161
|
+
if self.verbose:
|
162
|
+
print(err, x_values)
|
163
|
+
return err
|
164
|
+
|
165
|
+
def fit(self, x0, bounds, method="Nelder-Mead"):
|
166
|
+
"""Fit the model to the data.
|
167
|
+
|
168
|
+
Fit the model to the data using the objective function defined in
|
169
|
+
the objective_function method. The optimization is performed using
|
170
|
+
the scipy.optimize.minimize function.
|
171
|
+
The optimization result is stored in the `.solution` property. Which
|
172
|
+
|
173
|
+
Parameters
|
174
|
+
----------
|
175
|
+
x0 : array-like
|
176
|
+
Initial guess for the fitting parameters.
|
177
|
+
bounds : array-like
|
178
|
+
Bounds for the fitting parameters.
|
179
|
+
method : str, optional
|
180
|
+
The optimization method to use. Default is 'Nelder-Mead'.
|
181
|
+
|
182
|
+
Returns
|
183
|
+
-------
|
184
|
+
None
|
185
|
+
"""
|
186
|
+
sol = minimize(
|
187
|
+
self.objective_function, x0=x0, bounds=bounds, method=method
|
188
|
+
)
|
189
|
+
self._solution = sol
|
190
|
+
|
191
|
+
@property
|
192
|
+
def solution(self):
|
193
|
+
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
|