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/core.py ADDED
@@ -0,0 +1,2473 @@
1
+ """yaeos Python API core module.
2
+
3
+ ArModel and GeModel abstract classes definition. Also, the implementation of
4
+ the models' thermoprops methods.
5
+ """
6
+
7
+ from abc import ABC
8
+ from typing import Union
9
+
10
+ from intersect import intersection
11
+
12
+ import numpy as np
13
+
14
+ from yaeos.lib import yaeos_c
15
+
16
+ from yaeos.envelopes import PTEnvelope, PXEnvelope, TXEnvelope
17
+
18
+
19
+ class GeModel(ABC):
20
+ """Excess Gibbs (Ge) model abstract class."""
21
+
22
+ def ln_gamma(
23
+ self, moles, temperature: float, dt: bool = False, dn: bool = False
24
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
25
+ r"""Calculate natural logarithm of activity coefficients.
26
+
27
+ Calculate :math:`\ln \gamma_i(n,T)` vector.
28
+
29
+ Parameters
30
+ ----------
31
+ moles : array_like
32
+ Moles number vector [mol]
33
+ temperature : float
34
+ Temperature [K]
35
+
36
+ Returns
37
+ -------
38
+ np.ndarray
39
+ :math:`ln \gamma_i(n,T)` vector
40
+
41
+ Example
42
+ -------
43
+ .. code-block:: python
44
+
45
+ import numpy as np
46
+
47
+ from yaeos import NRTL
48
+
49
+
50
+ a = np.array([[0, 0.3], [0.3, 0]])
51
+ b = np.array([[0, 0.4], [0.4, 0]])
52
+ c = np.array([[0, 0.5], [0.5, 0]])
53
+
54
+ nrtl = NRTL(a, b, c)
55
+
56
+ # Evaluating ln_gamma only
57
+ print(nrtl.ln_gamma([5.0, 5.6], 300.0))
58
+
59
+ # Asking for derivatives
60
+
61
+ print(nrtl.ln_gamma([5.0, 5.6], 300.0, dt=True, dn=True))
62
+ """
63
+ nc = len(moles)
64
+
65
+ dt = np.empty(nc, order="F") if dt else None
66
+ dn = np.empty((nc, nc), order="F") if dn else None
67
+
68
+ res = yaeos_c.ln_gamma_ge(
69
+ self.id,
70
+ moles,
71
+ temperature,
72
+ dlngamma_dt=dt,
73
+ dlngamma_dn=dn,
74
+ )
75
+
76
+ if dt is None and dn is None:
77
+ ...
78
+ else:
79
+ res = (res, {"dt": dt, "dn": dn})
80
+ return res
81
+
82
+ def excess_gibbs(
83
+ self,
84
+ moles,
85
+ temperature: float,
86
+ dt: bool = False,
87
+ dt2: bool = False,
88
+ dn: bool = False,
89
+ dtn: bool = False,
90
+ dn2: bool = False,
91
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
92
+ """Calculate excess Gibbs energy [bar L].
93
+
94
+ Parameters
95
+ ----------
96
+ moles : array_like
97
+ Moles number vector [mol]
98
+ temperature : float
99
+ Temperature [K]
100
+ dt : bool, optional
101
+ Calculate temperature derivative, by default False
102
+ dt2 : bool, optional
103
+ Calculate temperature second derivative, by default False
104
+ dn : bool, optional
105
+ Calculate moles derivative, by default False
106
+ dtn : bool, optional
107
+ Calculate cross temperature and moles derivative, by default False
108
+ dn2 : bool, optional
109
+ Calculate moles second derivative, by default False
110
+
111
+ Returns
112
+ -------
113
+ Union[np.ndarray, tuple[np.ndarray, dict]]
114
+ Excess Gibbs energy or tuple with excess Gibbs energy and
115
+ derivatives dictionary if any derivative is asked [bar L]
116
+
117
+ Example
118
+ -------
119
+ .. code-block:: python
120
+
121
+ from yaeos import UNIFACVLE
122
+
123
+ # Ethanol - water system
124
+ groups = [{1: 2, 2: 1, 14: 1}, {16: 1}]
125
+
126
+ model = UNIFACVLE(groups)
127
+
128
+ # Evaluating excess Gibbs energy only
129
+ print(model.excess_gibbs(model.excess_gibbs([0.5,0.5], 303.15))
130
+
131
+ # Asking for derivatives
132
+ print(
133
+ model.excess_gibbs(
134
+ [0.5,0.5],
135
+ 303.15,
136
+ dt=True,
137
+ dt2=True,
138
+ dn=True,
139
+ dtn=True,
140
+ dn2=True
141
+ )
142
+ """
143
+ nc = len(moles)
144
+
145
+ dt = np.empty(1, order="F") if dt else None
146
+ dt2 = np.empty(1, order="F") if dt2 else None
147
+ dn = np.empty(nc, order="F") if dn else None
148
+ dtn = np.empty(nc, order="F") if dtn else None
149
+ dn2 = np.empty((nc, nc), order="F") if dn2 else None
150
+
151
+ possible_derivatives = [dt, dt2, dn, dtn]
152
+ all_none = all([d is None for d in possible_derivatives])
153
+
154
+ res = yaeos_c.excess_gibbs_ge(
155
+ self.id,
156
+ moles,
157
+ temperature,
158
+ get=dt,
159
+ get2=dt2,
160
+ gen=dn,
161
+ getn=dtn,
162
+ gen2=dn2,
163
+ )
164
+
165
+ if all_none:
166
+ ...
167
+ else:
168
+ res = (
169
+ res,
170
+ {
171
+ "dt": dt if dt is None else dt[0],
172
+ "dt2": dt2 if dt2 is None else dt2[0],
173
+ "dn": dn,
174
+ "dtn": dtn,
175
+ "dn2": dn2,
176
+ },
177
+ )
178
+
179
+ return res
180
+
181
+ def excess_enthalpy(
182
+ self, moles, temperature: float, dt: bool = False, dn: bool = False
183
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
184
+ """Calculate excess enthalpy [bar L].
185
+
186
+ Parameters
187
+ ----------
188
+ moles : array_like
189
+ Moles number vector [mol]
190
+ temperature : float
191
+ Temperature [K]
192
+ dt : bool, optional
193
+ Calculate temperature derivative, by default False
194
+ dn : bool, optional
195
+ Calculate moles derivative, by default False
196
+
197
+ Returns
198
+ -------
199
+ Union[np.ndarray, tuple[np.ndarray, dict]]
200
+ Excess enthalpy or tuple with excess enthalpy and derivatives
201
+ dictionary if any derivative is asked [bar L]
202
+
203
+ Example
204
+ -------
205
+ .. code-block:: python
206
+
207
+ from yaeos import UNIFACVLE
208
+
209
+ # Ethanol - water system
210
+ groups = [{1: 2, 2: 1, 14: 1}, {16: 1}]
211
+
212
+ model = UNIFACVLE(groups)
213
+
214
+ # Evaluating excess enthalpy only
215
+ print(model.excess_enthalpy([0.5, 0.5], 303.15))
216
+
217
+ # Asking for derivatives
218
+ print(model.excess_enthalpy([0.5, 0.5], 303.15, dt=True, dn=True))
219
+ """
220
+ nc = len(moles)
221
+
222
+ dt = np.empty(1, order="F") if dt else None
223
+ dn = np.empty(nc, order="F") if dn else None
224
+
225
+ res = yaeos_c.excess_enthalpy_ge(
226
+ self.id,
227
+ moles,
228
+ temperature,
229
+ het=dt,
230
+ hen=dn,
231
+ )
232
+
233
+ if dt is None and dn is None:
234
+ ...
235
+ else:
236
+ res = (res, {"dt": dt if dt is None else dt[0], "dn": dn})
237
+
238
+ return res
239
+
240
+ def excess_entropy(
241
+ self, moles, temperature: float, dt: bool = False, dn: bool = False
242
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
243
+ """Calculate excess entropy [bar L / K].
244
+
245
+ Parameters
246
+ ----------
247
+ moles : array_like
248
+ Moles number vector [mol]
249
+ temperature : float
250
+ Temperature [K]
251
+ dt : bool, optional
252
+ Calculate temperature derivative, by default False
253
+ dn : bool, optional
254
+ Calculate moles derivative, by default False
255
+
256
+ Returns
257
+ -------
258
+ Union[np.ndarray, tuple[np.ndarray, dict]]
259
+ Excess entropy or tuple with excess entropy and derivatives
260
+ dictionary if any derivative is asked [bar L / K]
261
+
262
+ Example
263
+ -------
264
+ .. code-block:: python
265
+
266
+ from yaeos import UNIFACVLE
267
+
268
+ # Ethanol - water system
269
+ groups = [{1: 2, 2: 1, 14: 1}, {16: 1}]
270
+
271
+ model = UNIFACVLE(groups)
272
+
273
+ # Evaluating excess entropy only
274
+ print(model.excess_entropy([0.5, 0.5], 303.15))
275
+
276
+ # Asking for derivatives
277
+ print(model.excess_entropy([0.5, 0.5], 303.15, dt=True, dn=True))
278
+ """
279
+ nc = len(moles)
280
+
281
+ dt = np.empty(1, order="F") if dt else None
282
+ dn = np.empty(nc, order="F") if dn else None
283
+
284
+ res = yaeos_c.excess_entropy_ge(
285
+ self.id,
286
+ moles,
287
+ temperature,
288
+ set=dt,
289
+ sen=dn,
290
+ )
291
+
292
+ if dt is None and dn is None:
293
+ ...
294
+ else:
295
+ res = (res, {"dt": dt if dt is None else dt[0], "dn": dn})
296
+
297
+ return res
298
+
299
+ def stability_analysis(self, z, temperature):
300
+ """Perform stability analysis.
301
+
302
+ Find all the possible minima values that the :math:`tm` function,
303
+ defined by Michelsen and Mollerup.
304
+
305
+ Parameters
306
+ ----------
307
+ z : array_like
308
+ Global mole fractions
309
+ temperature : float
310
+ Temperature [K]
311
+
312
+ Returns
313
+ -------
314
+ dict
315
+ Stability analysis result dictionary with keys:
316
+ - w: value of the test phase that minimizes the :math:`tm` function
317
+ - tm: minimum value of the :math:`tm` function.
318
+ dict
319
+ All found minimum values of the :math:`tm` function and the
320
+ corresponding test phase mole fractions.
321
+ - w: all values of :math:`w` that minimize the :math:`tm` function
322
+ - tm: all values found minima of the :math:`tm` function"""
323
+ (w_min, tm_min, all_mins) = yaeos_c.stability_zt_ge(
324
+ id=self.id, z=z, t=temperature
325
+ )
326
+
327
+ all_mins_w = all_mins[:, : len(z)]
328
+ all_mins = all_mins[:, -1]
329
+
330
+ return {"w": w_min, "tm": tm_min}, {"tm": all_mins, "w": all_mins_w}
331
+
332
+ def flash_t(self, z, temperature: float, k0=None) -> dict:
333
+ """Two-phase split with specification of temperature and pressure.
334
+
335
+ Parameters
336
+ ----------
337
+ z : array_like
338
+ Global mole fractions
339
+ temperature : float
340
+ Temperature [K]
341
+ k0 : array_like, optional
342
+ Initial guess for the split, by default None (will use k_wilson)
343
+
344
+ Returns
345
+ -------
346
+ dict
347
+ Flash result dictionary with keys:
348
+ - x: heavy phase mole fractions
349
+ - y: light phase mole fractions
350
+ - Vx: heavy phase volume [L]
351
+ - Vy: light phase volume [L]
352
+ - T: temperature [K]
353
+ - beta: light phase fraction
354
+ """
355
+ if k0 is None:
356
+ mintpd, _ = self.stability_analysis(z, temperature)
357
+ k0 = mintpd["w"] / np.array(z)
358
+
359
+ x, y, pressure, temperature, volume_x, volume_y, beta = (
360
+ yaeos_c.flash_ge(self.id, z, t=temperature, k0=k0)
361
+ )
362
+
363
+ flash_result = {
364
+ "x": x,
365
+ "y": y,
366
+ "Vx": volume_x,
367
+ "Vy": volume_y,
368
+ "T": temperature,
369
+ "beta": beta,
370
+ }
371
+
372
+ return flash_result
373
+
374
+ def __del__(self) -> None:
375
+ """Delete the model from the available models list (Fortran side)."""
376
+ yaeos_c.make_available_ge_models_list(self.id)
377
+
378
+
379
+ class ArModel(ABC):
380
+ """Residual Helmholtz (Ar) model abstract class."""
381
+
382
+ def lnphi_vt(
383
+ self,
384
+ moles,
385
+ volume: float,
386
+ temperature: float,
387
+ dt: bool = False,
388
+ dp: bool = False,
389
+ dn: bool = False,
390
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
391
+ r"""Calculate fugacity coefficent given volume and temperature.
392
+
393
+ Calculate :math:`ln \phi_i(n,V,T)` and its derivatives with respect to
394
+ temperature, pressure and moles number.
395
+
396
+ Parameters
397
+ ----------
398
+ moles : array_like
399
+ Moles number vector [mol]
400
+ volume : float
401
+ Volume [L]
402
+ temperature : float
403
+ Temperature [K]
404
+ dt : bool, optional
405
+ Calculate temperature derivative, by default False
406
+ dp : bool, optional
407
+ Calculate pressure derivative, by default False
408
+ dn : bool, optional
409
+ Calculate moles derivative, by default False
410
+
411
+ Returns
412
+ -------
413
+ Union[np.ndarray, tuple[np.ndarray, dict]]
414
+ :math:`ln \phi_i(n,V,T)` vector or tuple with
415
+ :math:`ln \phi_i(n,V,T)` vector and derivatives dictionary if any
416
+ derivative is asked
417
+
418
+ Example
419
+ -------
420
+ .. code-block:: python
421
+
422
+ import numpy as np
423
+
424
+ from yaeos import PengRobinson76
425
+
426
+
427
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
428
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
429
+ w = np.array([0.0123, 0.045]) # acentric factors
430
+
431
+ model = PengRobinson76(tc, pc, w)
432
+
433
+ # Evaluating ln_phi only
434
+ # will print: [-1.45216274 -2.01044828]
435
+
436
+ print(model.lnphi_vt([5.0, 5.6], 1.0, 300.0))
437
+
438
+ # Asking for derivatives
439
+ # will print:
440
+ # (
441
+ # array([-1.45216274, -2.01044828]),
442
+ # {'dt': array([0.01400063, 0.01923493]), 'dp': None, 'dn': None}
443
+ # )
444
+
445
+ print(model.lnphi_vt([5.0, 5.6], 1.0, 300.0, dt=True)
446
+ """
447
+ nc = len(moles)
448
+
449
+ dt = np.empty(nc, order="F") if dt else None
450
+ dp = np.empty(nc, order="F") if dp else None
451
+ dn = np.empty((nc, nc), order="F") if dn else None
452
+
453
+ res = yaeos_c.lnphi_vt(
454
+ self.id,
455
+ moles,
456
+ volume,
457
+ temperature,
458
+ dlnphidt=dt,
459
+ dlnphidp=dp,
460
+ dlnphidn=dn,
461
+ )
462
+
463
+ if dt is None and dp is None and dn is None:
464
+ ...
465
+ else:
466
+ res = (res, {"dt": dt, "dp": dp, "dn": dn})
467
+ return res
468
+
469
+ def lnphi_pt(
470
+ self,
471
+ moles,
472
+ pressure: float,
473
+ temperature: float,
474
+ root: str = "stable",
475
+ dt: bool = False,
476
+ dp: bool = False,
477
+ dn: bool = False,
478
+ ) -> Union[np.ndarray, tuple[np.ndarray, dict]]:
479
+ r"""Calculate fugacity coefficent given pressure and temperature.
480
+
481
+ Calculate :math:`ln \phi_i(n,P,T)` and its derivatives with respect to
482
+ temperature, pressure and moles number.
483
+
484
+ Parameters
485
+ ----------
486
+ moles : array_like
487
+ Moles number vector [mol]
488
+ pressure : float
489
+ Pressure [bar]
490
+ temperature : float
491
+ Temperature [K]
492
+ root : str, optional
493
+ Volume root, use: "liquid", "vapor" or "stable", by default
494
+ "stable"
495
+ dt : bool, optional
496
+ Calculate temperature derivative, by default False
497
+ dp : bool, optional
498
+ Calculate pressure derivative, by default False
499
+ dn : bool, optional
500
+ Calculate moles derivative, by default False
501
+
502
+ Returns
503
+ -------
504
+ Union[np.ndarray, tuple[np.ndarray, dict]]
505
+ :math:`ln \phi_i(n,P,T)` vector or tuple with
506
+ :math:`ln \phi_i(n,P,T)` vector and derivatives dictionary if any
507
+ derivative is asked
508
+
509
+ Example
510
+ -------
511
+ .. code-block:: python
512
+
513
+ import numpy as np
514
+
515
+ from yaeos import PengRobinson76
516
+
517
+
518
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
519
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
520
+ w = np.array([0.0123, 0.045]) # acentric factors
521
+
522
+ model = PengRobinson76(tc, pc, w)
523
+
524
+ # Evaluating ln_phi only
525
+ # will print: [-0.10288733 -0.11909807]
526
+
527
+ print(model.lnphi_pt([5.0, 5.6], 10.0, 300.0))
528
+
529
+ # Asking for derivatives
530
+ # will print:
531
+ # (
532
+ # array([-0.10288733, -0.11909807]),
533
+ # {'dt': array([0.00094892, 0.00108809]), 'dp': None, 'dn': None}
534
+ # )
535
+
536
+ print(model.lnphi_pt([5.0, 5.6], 10.0, 300.0, dt=True)
537
+ """
538
+ nc = len(moles)
539
+
540
+ dt = np.empty(nc, order="F") if dt else None
541
+ dp = np.empty(nc, order="F") if dp else None
542
+ dn = np.empty((nc, nc), order="F") if dn else None
543
+
544
+ res = yaeos_c.lnphi_pt(
545
+ self.id,
546
+ moles,
547
+ pressure,
548
+ temperature,
549
+ root,
550
+ dlnphidt=dt,
551
+ dlnphidp=dp,
552
+ dlnphidn=dn,
553
+ )
554
+
555
+ if dt is None and dp is None and dn is None:
556
+ ...
557
+ else:
558
+ res = (res, {"dt": dt, "dp": dp, "dn": dn})
559
+ return res
560
+
561
+ def pressure(
562
+ self,
563
+ moles,
564
+ volume: float,
565
+ temperature: float,
566
+ dv: bool = False,
567
+ dt: bool = False,
568
+ dn: bool = False,
569
+ ) -> Union[float, tuple[float, dict]]:
570
+ """Calculate pressure given volume and temperature [bar].
571
+
572
+ Calculate :math:`P(n,V,T)` and its derivatives with respect to
573
+ volume, temperature and moles number.
574
+
575
+ Parameters
576
+ ----------
577
+ moles : array_like
578
+ Moles number vector [mol]
579
+ volume : float
580
+ Volume [L]
581
+ temperature : float
582
+ Temperature [K]
583
+ dv : bool, optional
584
+ Calculate volume derivative, by default False
585
+ dt : bool, optional
586
+ Calculate temperature derivative, by default False
587
+ dn : bool, optional
588
+ Calculate moles derivative, by default False
589
+
590
+ Returns
591
+ -------
592
+ Union[float, tuple[float, dict]]
593
+ Pressure or tuple with Presure and derivatives dictionary if any
594
+ derivative is asked [bar]
595
+
596
+ Example
597
+ -------
598
+ .. code-block:: python
599
+
600
+ import numpy as np
601
+
602
+ from yaeos import PengRobinson76
603
+
604
+
605
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
606
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
607
+ w = np.array([0.0123, 0.045]) # acentric factors
608
+
609
+ model = PengRobinson76(tc, pc, w)
610
+
611
+ # Evaluating pressure only
612
+ # will print: 16.011985733846956
613
+
614
+ print(model.pressure(np.array([5.0, 5.6]), 2.0, 300.0))
615
+
616
+ # Asking for derivatives
617
+ # will print:
618
+ # (
619
+ # 16.011985733846956,
620
+ # {'dv': None, 'dt': np.float64(0.7664672352866752), 'dn': None}
621
+ # )
622
+
623
+ print(model.pressure(np.array([5.0, 5.6]), 2.0, 300.0, dt=True))
624
+ """
625
+ nc = len(moles)
626
+
627
+ dv = np.empty(1, order="F") if dv else None
628
+ dt = np.empty(1, order="F") if dt else None
629
+ dn = np.empty(nc, order="F") if dn else None
630
+
631
+ res = yaeos_c.pressure(
632
+ self.id, moles, volume, temperature, dpdv=dv, dpdt=dt, dpdn=dn
633
+ )
634
+
635
+ if dt is None and dv is None and dn is None:
636
+ ...
637
+ else:
638
+ res = (
639
+ res,
640
+ {
641
+ "dv": dv if dv is None else dv[0],
642
+ "dt": dt if dt is None else dt[0],
643
+ "dn": dn,
644
+ },
645
+ )
646
+ return res
647
+
648
+ def volume(
649
+ self, moles, pressure: float, temperature: float, root: str = "stable"
650
+ ) -> float:
651
+ """Calculate volume given pressure and temperature [L].
652
+
653
+ Parameters
654
+ ----------
655
+ moles : array_like
656
+ Moles number vector [mol]
657
+ pressure : float
658
+ Pressure [bar]
659
+ temperature : float
660
+ Temperature [K]
661
+ root : str, optional
662
+ Volume root, use: "liquid", "vapor" or "stable", by default
663
+ "stable"
664
+
665
+ Returns
666
+ -------
667
+ float
668
+ Volume [L]
669
+
670
+ Example
671
+ -------
672
+ .. code-block:: python
673
+
674
+ import numpy as np
675
+
676
+ from yaeos import PengRobinson76
677
+
678
+
679
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
680
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
681
+ w = np.array([0.0123, 0.045]) # acentric factors
682
+
683
+ model = PengRobinson76(tc, pc, w)
684
+
685
+ # Evaluating stable root volume
686
+ # will print: 23.373902973572587
687
+
688
+ print(model.volume(np.array([5.0, 5.6]), 10.0, 300.0))
689
+
690
+ # Liquid root volume (not stable)
691
+ # will print: 0.8156388756398074
692
+
693
+ print(model.volume(np.array([5.0, 5.6]), 10.0, 300.0, "liquid"))
694
+ """
695
+ res = yaeos_c.volume(self.id, moles, pressure, temperature, root)
696
+ return res
697
+
698
+ def enthalpy_residual_vt(
699
+ self,
700
+ moles,
701
+ volume: float,
702
+ temperature: float,
703
+ dt: bool = False,
704
+ dv: bool = False,
705
+ dn: bool = False,
706
+ ) -> Union[float, tuple[float, dict]]:
707
+ """Calculate residual enthalpy given volume and temperature [bar L].
708
+
709
+ Parameters
710
+ ----------
711
+ moles : array_like
712
+ Moles number vector [mol]
713
+ volume : float
714
+ Volume [L]
715
+ temperature : float
716
+ Temperature [K]
717
+
718
+ Returns
719
+ -------
720
+ Union[float, tuple[float, dict]]
721
+ Residual enthalpy or tuple with Residual enthalpy and derivatives
722
+ dictionary if any derivative is asked [bar L]
723
+
724
+ Example
725
+ -------
726
+ .. code-block:: python
727
+
728
+ import numpy as np
729
+
730
+ from yaeos import PengRobinson76
731
+
732
+
733
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
734
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
735
+ w = np.array([0.0123, 0.045]) # acentric factors
736
+
737
+ model = PengRobinson76(tc, pc, w)
738
+
739
+ # Evaluating residual enthalpy only
740
+ # will print: -182.50424367123696
741
+
742
+ print(
743
+ model.enthalpy_residual_vt(np.array([5.0, 5.6]), 10.0, 300.0)
744
+ )
745
+
746
+ # Asking for derivatives
747
+ # will print:
748
+ # (
749
+ # -182.50424367123696,
750
+ # {'dt': 0.21542452742588686, 'dv': None, 'dn': None}
751
+ # )
752
+
753
+ print(
754
+ model.enthalpy_residual_vt(
755
+ np.array([5.0, 5.6]),
756
+ 10.0,
757
+ 300.0,
758
+ dt=True)
759
+ )
760
+ )
761
+ """
762
+ nc = len(moles)
763
+
764
+ dt = np.empty(1, order="F") if dt else None
765
+ dv = np.empty(1, order="F") if dv else None
766
+ dn = np.empty(nc, order="F") if dn else None
767
+
768
+ res = yaeos_c.enthalpy_residual_vt(
769
+ self.id,
770
+ moles,
771
+ volume,
772
+ temperature,
773
+ hrt=dt,
774
+ hrv=dv,
775
+ hrn=dn,
776
+ )
777
+
778
+ if dt is None and dv is None and dn is None:
779
+ ...
780
+ else:
781
+ res = (
782
+ res,
783
+ {
784
+ "dt": dt if dt is None else dt[0],
785
+ "dv": dv if dv is None else dv[0],
786
+ "dn": dn,
787
+ },
788
+ )
789
+ return res
790
+
791
+ def gibbs_residual_vt(
792
+ self,
793
+ moles,
794
+ volume: float,
795
+ temperature: float,
796
+ dt: bool = False,
797
+ dv: bool = False,
798
+ dn: bool = False,
799
+ ) -> Union[float, tuple[float, dict]]:
800
+ """Calculate residual Gibbs energy at volume and temperature [bar L].
801
+
802
+ Parameters
803
+ ----------
804
+ moles : array_like
805
+ Moles number vector [mol]
806
+ volume : float
807
+ Volume [L]
808
+ temperature : float
809
+ Temperature [K]
810
+
811
+ Returns
812
+ -------
813
+ Union[float, tuple[float, dict]]
814
+ Residual Gibbs energy or tuple with Residual Gibbs energy and
815
+ derivatives dictionary if any derivative is asked [bar L]
816
+
817
+ Example
818
+ -------
819
+ .. code-block:: python
820
+
821
+ import numpy as np
822
+
823
+ from yaeos import PengRobinson76
824
+
825
+
826
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
827
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
828
+ w = np.array([0.0123, 0.045]) # acentric factors
829
+
830
+ model = PengRobinson76(tc, pc, w)
831
+
832
+ # Evaluating residual gibbs energy only
833
+ # will print: -138.60374582274
834
+
835
+ print(model.gibbs_residual_vt(np.array([5.0, 5.6]), 10.0, 300.0))
836
+
837
+ # Asking for derivatives
838
+ # will print:
839
+ # (
840
+ # -138.60374582274,
841
+ # {'dt': 0.289312908265414, 'dv': None, 'dn': None}
842
+ # )
843
+
844
+ print(
845
+ model.gibbs_residual_vt(
846
+ np.array([5.0, 5.6]),
847
+ 10.0,
848
+ 300.0,
849
+ dt=True
850
+ )
851
+ )
852
+ """
853
+ nc = len(moles)
854
+
855
+ dt = np.empty(1, order="F") if dt else None
856
+ dv = np.empty(1, order="F") if dv else None
857
+ dn = np.empty(nc, order="F") if dn else None
858
+
859
+ res = yaeos_c.gibbs_residual_vt(
860
+ self.id,
861
+ moles,
862
+ volume,
863
+ temperature,
864
+ grt=dt,
865
+ grv=dv,
866
+ grn=dn,
867
+ )
868
+
869
+ if dt is None and dv is None and dn is None:
870
+ ...
871
+ else:
872
+ res = (
873
+ res,
874
+ {
875
+ "dt": dt if dt is None else dt[0],
876
+ "dv": dv if dv is None else dv[0],
877
+ "dn": dn,
878
+ },
879
+ )
880
+ return res
881
+
882
+ def entropy_residual_vt(
883
+ self,
884
+ moles,
885
+ volume: float,
886
+ temperature: float,
887
+ dt: bool = False,
888
+ dv: bool = False,
889
+ dn: bool = False,
890
+ ) -> Union[float, tuple[float, dict]]:
891
+ """Calculate residual entropy given volume and temperature [bar L / K].
892
+
893
+ Parameters
894
+ ----------
895
+ moles : array_like
896
+ Moles number vector [mol]
897
+ volume : float
898
+ Volume [L]
899
+ temperature : float
900
+ Temperature [K]
901
+
902
+ Returns
903
+ -------
904
+ Union[float, tuple[float, dict]]
905
+ Residual entropy or tuple with Residual entropy and derivatives
906
+ dictionary if any derivative is asked [bar L]
907
+
908
+ Example
909
+ -------
910
+ .. code-block:: python
911
+
912
+ import numpy as np
913
+
914
+ from yaeos import PengRobinson76
915
+
916
+
917
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
918
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
919
+ w = np.array([0.0123, 0.045]) # acentric factors
920
+
921
+ model = PengRobinson76(tc, pc, w)
922
+
923
+ # Evaluating residual entropy only
924
+ # will print: -0.1463349928283233
925
+
926
+ print(model.entropy_residual_vt(np.array([5.0, 5.6]), 10.0, 300.0))
927
+
928
+ # Asking for derivatives
929
+ # will print:
930
+ # (
931
+ # (-0.1463349928283233,
932
+ # {'dt': 0.00024148870662932045, 'dv': None, 'dn': None})
933
+ # )
934
+
935
+ print(
936
+ model.entropy_residual_vt(
937
+ np.array([5.0, 5.6]),
938
+ 10.0,
939
+ 300.0,
940
+ dt=True
941
+ )
942
+ )
943
+ """
944
+ nc = len(moles)
945
+
946
+ dt = np.empty(1, order="F") if dt else None
947
+ dv = np.empty(1, order="F") if dv else None
948
+ dn = np.empty(nc, order="F") if dn else None
949
+
950
+ res = yaeos_c.entropy_residual_vt(
951
+ self.id,
952
+ moles,
953
+ volume,
954
+ temperature,
955
+ srt=dt,
956
+ srv=dv,
957
+ srn=dn,
958
+ )
959
+
960
+ if dt is None and dv is None and dn is None:
961
+ ...
962
+ else:
963
+ res = (
964
+ res,
965
+ {
966
+ "dt": dt if dt is None else dt[0],
967
+ "dv": dv if dv is None else dv[0],
968
+ "dn": dn,
969
+ },
970
+ )
971
+ return res
972
+
973
+ def cv_residual_vt(
974
+ self, moles, volume: float, temperature: float
975
+ ) -> float:
976
+ """Residual isochoric heat capacity given V and T [bar L / K].
977
+
978
+ Parameters
979
+ ----------
980
+ moles : array_like
981
+ Moles number vector [mol]
982
+ volume : float
983
+ Volume [L]
984
+ temperature : float
985
+ Temperature [K]
986
+
987
+ Returns
988
+ -------
989
+ float
990
+ Residual isochoric heat capacity [bar L / K]
991
+
992
+ Example
993
+ -------
994
+ .. code-block:: python
995
+
996
+ import numpy as np
997
+
998
+ from yaeos import PengRobinson76
999
+
1000
+
1001
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
1002
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
1003
+ w = np.array([0.0123, 0.045]) # acentric factors
1004
+
1005
+ model = PengRobinson76(tc, pc, w)
1006
+
1007
+ # Evaluating residual isochoric heat capacity only
1008
+ # will print: 0.07244661198879614
1009
+
1010
+ print(model.cv_residual_vt(np.array([5.0, 5.6]), 10.0, 300.0))
1011
+ """
1012
+ return yaeos_c.cv_residual_vt(self.id, moles, volume, temperature)
1013
+
1014
+ def cp_residual_vt(
1015
+ self, moles, volume: float, temperature: float
1016
+ ) -> float:
1017
+ """Calculate residual isobaric heat capacity given V and T [bar L / K].
1018
+
1019
+ Parameters
1020
+ ----------
1021
+ moles : array_like
1022
+ Moles number vector [mol]
1023
+ volume : float
1024
+ Volume [L]
1025
+ temperature : float
1026
+ Temperature [K]
1027
+
1028
+ Returns
1029
+ -------
1030
+ float
1031
+ Residual isochoric heat capacity [bar L / K]
1032
+
1033
+ Example
1034
+ -------
1035
+ .. code-block:: python
1036
+
1037
+ import numpy as np
1038
+
1039
+ from yaeos import PengRobinson76
1040
+
1041
+
1042
+ tc = np.array([320.0, 375.0]) # critical temperatures [K]
1043
+ pc = np.array([45.0, 60.0]) # critical pressures [bar]
1044
+ w = np.array([0.0123, 0.045]) # acentric factors
1045
+
1046
+ model = PengRobinson76(tc, pc, w)
1047
+
1048
+ # Evaluating residual isobaric heat capacity only
1049
+ # will print: 1.4964025088916886
1050
+
1051
+ print(model.cp_residual_vt(np.array([5.0, 5.6]), 10.0, 300.0))
1052
+ """
1053
+ return yaeos_c.cp_residual_vt(self.id, moles, volume, temperature)
1054
+
1055
+ def pure_saturation_pressures(
1056
+ self, component, stop_pressure=0.01, stop_temperature=100
1057
+ ):
1058
+ """Calculate pure component saturation pressures [bar].
1059
+
1060
+ Calculation starts from the critical point and goes down to the
1061
+ stop pressure or stop temperature.
1062
+
1063
+ Parameters
1064
+ ----------
1065
+ component : int
1066
+ Component index (starting from 1)
1067
+ stop_pressure : float, optional
1068
+ Stop pressure [bar], by default 0.01
1069
+ stop_temperature : float, optional
1070
+ Stop temperature [K], by default 100
1071
+
1072
+ Returns
1073
+ -------
1074
+ dict
1075
+ Pure component saturation points dictionary with keys:
1076
+ - T: Temperature [K]
1077
+ - P: Pressure [bar]
1078
+ - Vx: Liquid Phase Volume [L/mole]
1079
+ - Vy: Vapor Phase Volume [L/mole]
1080
+
1081
+ Example
1082
+ -------
1083
+ .. code-block:: python
1084
+
1085
+ import numpy as np
1086
+
1087
+ from yaeos import PengRobinson76
1088
+
1089
+ tc = np.array([320.0, 375.0])
1090
+ pc = np.array([45.0, 60.0])
1091
+ w = np.array([0.0123, 0.045])
1092
+
1093
+ model = PengRobinson76(tc, pc, w)
1094
+ """
1095
+ p, t, vx, vy = yaeos_c.pure_saturation_line(
1096
+ self.id, component, stop_p=stop_pressure, stop_t=stop_temperature
1097
+ )
1098
+
1099
+ msk = ~np.isnan(t)
1100
+
1101
+ return {"T": t[msk], "P": p[msk], "Vx": vx[msk], "Vy": vy[msk]}
1102
+
1103
+ def flash_pt(
1104
+ self, z, pressure: float, temperature: float, k0=None
1105
+ ) -> dict:
1106
+ """Two-phase split with specification of temperature and pressure.
1107
+
1108
+ Parameters
1109
+ ----------
1110
+ z : array_like
1111
+ Global mole fractions
1112
+ pressure : float
1113
+ Pressure [bar]
1114
+ temperature : float
1115
+ Temperature [K]
1116
+ k0 : array_like, optional
1117
+ Initial guess for the split, by default None (will use k_wilson)
1118
+
1119
+ Returns
1120
+ -------
1121
+ dict
1122
+ Flash result dictionary with keys:
1123
+ - x: heavy phase mole fractions
1124
+ - y: light phase mole fractions
1125
+ - Vx: heavy phase volume [L]
1126
+ - Vy: light phase volume [L]
1127
+ - P: pressure [bar]
1128
+ - T: temperature [K]
1129
+ - beta: light phase fraction
1130
+
1131
+ Example
1132
+ -------
1133
+ .. code-block:: python
1134
+
1135
+ import numpy as np
1136
+
1137
+ from yaeos import PengRobinson76
1138
+
1139
+
1140
+ tc = np.array([369.83, 507.6]) # critical temperatures [K]
1141
+ pc = np.array([42.48, 30.25]) # critical pressures [bar]
1142
+ w = np.array([0.152291, 0.301261]) # acentric factors
1143
+
1144
+ model = PengRobinson76(tc, pc, w)
1145
+
1146
+ # Flash calculation
1147
+ # will print:
1148
+ # {
1149
+ # 'x': array([0.3008742, 0.6991258]),
1150
+ # 'y': array([0.85437317, 0.14562683]),
1151
+ # 'Vx': 0.12742569165483714,
1152
+ # 'Vy': 3.218831515959867,
1153
+ # 'P': 8.0,
1154
+ # 'T': 350.0,
1155
+ # 'beta': 0.35975821044266726
1156
+ # }
1157
+
1158
+ print(model.flash_pt([0.5, 0.5], 8.0, 350.0))
1159
+ """
1160
+ if k0 is None:
1161
+ k0 = [0 for i in range(len(z))]
1162
+ x, y, pressure, temperature, volume_x, volume_y, beta = yaeos_c.flash(
1163
+ self.id, z, p=pressure, t=temperature, k0=k0
1164
+ )
1165
+
1166
+ flash_result = {
1167
+ "x": x,
1168
+ "y": y,
1169
+ "Vx": volume_x,
1170
+ "Vy": volume_y,
1171
+ "P": pressure,
1172
+ "T": temperature,
1173
+ "beta": beta,
1174
+ }
1175
+
1176
+ return flash_result
1177
+
1178
+ def flash_pt_grid(
1179
+ self, z, pressures, temperatures, parallel=False
1180
+ ) -> dict:
1181
+ """Two-phase split with specification of temperature and pressure grid.
1182
+
1183
+ Parameters
1184
+ ----------
1185
+ z : array_like
1186
+ Global mole fractions
1187
+ pressures : array_like
1188
+ Pressures grid [bar]
1189
+ temperatures : array_like
1190
+ Temperatures grid [K]
1191
+ parallel : bool, optional
1192
+ Use parallel processing, by default False
1193
+
1194
+ Returns
1195
+ -------
1196
+ dict
1197
+ Flash grid result dictionary with keys:
1198
+ - x: heavy phase mole fractions
1199
+ - y: light phase mole fractions
1200
+ - Vx: heavy phase volume [L]
1201
+ - Vy: light phase volume [L]
1202
+ - P: pressure [bar]
1203
+ - T: temperature [K]
1204
+ - beta: light phase fraction
1205
+
1206
+ Example
1207
+ -------
1208
+ .. code-block:: python
1209
+
1210
+ import numpy as np
1211
+
1212
+ from yaeos import PengRobinson76
1213
+
1214
+ tc = np.array([369.83, 507.6]) # critical temperatures [K]
1215
+ pc = np.array([42.48, 30.25]) # critical pressures [bar]
1216
+ w = np.array([0.152291, 0.301261])
1217
+
1218
+ temperatures = [350.0, 360.0, 400.0]
1219
+ pressures = [10, 20, 30]
1220
+ """
1221
+ xs, ys, vxs, vys, betas = yaeos_c.flash_grid(
1222
+ self.id, z, pressures, temperatures, parallel=parallel
1223
+ )
1224
+
1225
+ flash = {
1226
+ "x": xs,
1227
+ "y": ys,
1228
+ "Vx": vxs,
1229
+ "Vy": vys,
1230
+ "P": pressures,
1231
+ "T": temperatures,
1232
+ "beta": betas,
1233
+ }
1234
+
1235
+ return flash
1236
+
1237
+ def saturation_pressure(
1238
+ self,
1239
+ z,
1240
+ temperature: float,
1241
+ kind: str = "bubble",
1242
+ p0: float = 0,
1243
+ y0=None,
1244
+ ) -> dict:
1245
+ """Saturation pressure at specified temperature.
1246
+
1247
+ Arguments
1248
+ ---------
1249
+ z: array_like
1250
+ Global molar fractions
1251
+ temperature: float
1252
+ Temperature [K]
1253
+ kind: str, optional
1254
+ Kind of saturation point, defaults to "bubble". Options are
1255
+ - "bubble"
1256
+ - "dew"
1257
+ - "liquid-liquid"
1258
+ p0: float, optional
1259
+ Initial guess for pressure [bar]
1260
+ y0: array_like, optional
1261
+ Initial guess for the incipient phase, by default None
1262
+ (will use k_wilson correlation)
1263
+
1264
+ Returns
1265
+ -------
1266
+ dict
1267
+ Saturation pressure calculation result dictionary with keys:
1268
+ - x: heavy phase mole fractions
1269
+ - y: light phase mole fractions
1270
+ - Vx: heavy phase volume [L]
1271
+ - Vy: light phase volume [L]
1272
+ - P: pressure [bar]
1273
+ - T: temperature [K]
1274
+ - beta: light phase fraction
1275
+
1276
+ Example
1277
+ -------
1278
+ .. code-block:: python
1279
+
1280
+ import numpy as np
1281
+
1282
+ from yaeos import PengRobinson76
1283
+
1284
+
1285
+ tc = np.array([369.83, 507.6]) # critical temperatures [K]
1286
+ pc = np.array([42.48, 30.25]) # critical pressures [bar]
1287
+ w = np.array([0.152291, 0.301261]) # acentric factors
1288
+
1289
+ model = PengRobinson76(tc, pc, w)
1290
+
1291
+ # Saturation pressure calculation
1292
+ # will print:
1293
+ # {
1294
+ # 'x': array([0.5, 0.5]),
1295
+ # 'y': array([0.9210035 , 0.07899651]),
1296
+ # 'Vx': 0.11974125553488875,
1297
+ # 'Vy': 1.849650524323853,
1298
+ # 'T': 350.0,
1299
+ # 'P': 12.990142036059941,
1300
+ # 'beta': 0.0
1301
+ # }
1302
+
1303
+ print(model.saturation_pressure(np.array([0.5, 0.5]), 350.0))
1304
+ """
1305
+ if y0 is None:
1306
+ y0 = np.zeros_like(z)
1307
+
1308
+ p, x, y, volume_x, volume_y, beta = yaeos_c.saturation_pressure(
1309
+ id=self.id, z=z, t=temperature, kind=kind, p0=p0, y0=y0
1310
+ )
1311
+
1312
+ return {
1313
+ "x": x,
1314
+ "y": y,
1315
+ "Vx": volume_x,
1316
+ "Vy": volume_y,
1317
+ "T": temperature,
1318
+ "P": p,
1319
+ "beta": beta,
1320
+ }
1321
+
1322
+ def saturation_temperature(
1323
+ self, z, pressure: float, kind: str = "bubble", t0: float = 0, y0=None
1324
+ ) -> dict:
1325
+ """Saturation temperature at specified pressure.
1326
+
1327
+ Arguments
1328
+ ---------
1329
+ z: array_like
1330
+ Global molar fractions
1331
+ pressure: float
1332
+ Pressure [bar]
1333
+ kind: str, optional
1334
+ Kind of saturation point, defaults to "bubble". Options are
1335
+ - "bubble"
1336
+ - "dew"
1337
+ - "liquid-liquid"
1338
+ t0: float, optional
1339
+ Initial guess for temperature [K]
1340
+ y0: array_like, optional
1341
+ Initial guess for the incipient phase, by default None
1342
+ (will use k_wilson correlation)
1343
+
1344
+ Returns
1345
+ -------
1346
+ dict
1347
+ Saturation temperature calculation result dictionary with keys:
1348
+ - x: heavy phase mole fractions
1349
+ - y: light phase mole fractions
1350
+ - Vx: heavy phase volume [L]
1351
+ - Vy: light phase volume [L]
1352
+ - P: pressure [bar]
1353
+ - T: temperature [K]
1354
+ - beta: light phase fraction
1355
+
1356
+ Example
1357
+ -------
1358
+ .. code-block:: python
1359
+
1360
+ import numpy as np
1361
+
1362
+ from yaeos import PengRobinson76
1363
+
1364
+
1365
+ tc = np.array([369.83, 507.6]) # critical temperatures [K]
1366
+ pc = np.array([42.48, 30.25]) # critical pressures [bar]
1367
+ w = np.array([0.152291, 0.301261]) # acentric factors
1368
+
1369
+ model = PengRobinson76(tc, pc, w)
1370
+
1371
+ # Saturation pressure calculation
1372
+ # will print:
1373
+ # {
1374
+ # 'x': array([0.5, 0.5]),
1375
+ # 'y': array([0.9210035 , 0.07899651]),
1376
+ # 'Vx': 0.11974125553488875,
1377
+ # 'Vy': 1.849650524323853,
1378
+ # 'T': 350.0,
1379
+ # 'P': 12.99,
1380
+ # 'beta': 0.0
1381
+ # }
1382
+
1383
+ print(model.saturation_temperature(np.array([0.5, 0.5]), 12.99))
1384
+ """
1385
+ if y0 is None:
1386
+ y0 = np.zeros_like(z)
1387
+
1388
+ t, x, y, volume_x, volume_y, beta = yaeos_c.saturation_temperature(
1389
+ id=self.id, z=z, p=pressure, kind=kind, t0=t0, y0=y0
1390
+ )
1391
+
1392
+ return {
1393
+ "x": x,
1394
+ "y": y,
1395
+ "Vx": volume_x,
1396
+ "Vy": volume_y,
1397
+ "T": t,
1398
+ "P": pressure,
1399
+ "beta": beta,
1400
+ }
1401
+
1402
+ # =========================================================================
1403
+ # Phase envelopes
1404
+ # -------------------------------------------------------------------------
1405
+ def phase_envelope_pt(
1406
+ self,
1407
+ z,
1408
+ kind: str = "bubble",
1409
+ max_points: int = 300,
1410
+ t0: float = 150.0,
1411
+ p0: float = 1.0,
1412
+ stop_pressure: float = 2500,
1413
+ ) -> dict:
1414
+ """Two phase envelope calculation (PT).
1415
+
1416
+ Parameters
1417
+ ----------
1418
+ z : array_like
1419
+ Global mole fractions
1420
+ kind : str, optional
1421
+ Kind of saturation point to start the envelope calculation,
1422
+ defaults to "bubble". Options are
1423
+ - "bubble"
1424
+ - "dew"
1425
+ max_points : int, optional
1426
+ Envelope's maximum points to calculate (T, P), by default 300
1427
+ t0 : float, optional
1428
+ Initial guess for temperature [K] for the saturation point of kind:
1429
+ `kind`, by default 150.0
1430
+ p0 : float, optional
1431
+ Initial guess for pressure [bar] for the saturation point of kind:
1432
+ `kind`, by default 1.0
1433
+ stop_pressure : float, optional
1434
+ Stop on pressures above stop_pressure [bar], by default 2500.0
1435
+
1436
+ Returns
1437
+ -------
1438
+ PTEnvelope
1439
+ PTEnvelope object with the phase envelope information.
1440
+
1441
+ Example
1442
+ -------
1443
+ .. code-block:: python
1444
+
1445
+ import numpy as np
1446
+
1447
+ import matplotlib.pyplot as plt
1448
+
1449
+ from yaeos import PengRobinson76
1450
+
1451
+
1452
+ tc = np.array([369.83, 507.6]) # critical temperatures [K]
1453
+ pc = np.array([42.48, 30.25]) # critical pressures [bar]
1454
+ w = np.array([0.152291, 0.301261]) # acentric factors
1455
+
1456
+ model = PengRobinson76(tc, pc, w)
1457
+
1458
+ # Two phase envelope calculation and plot
1459
+ env = model.phase_envelope_pt(
1460
+ np.array([0.5, 0.5]),
1461
+ t0=150.0,
1462
+ p0=1.0
1463
+ )
1464
+
1465
+ plt.plot(env["T"], env["P"])
1466
+ plt.scatter(env["Tc"], env["Pc"])
1467
+ """
1468
+
1469
+ if kind == "bubble":
1470
+ sat = self.saturation_pressure(z, t0, kind=kind, p0=p0)
1471
+ w0 = sat["y"]
1472
+ ns0 = len(z) + 3
1473
+ t0 = sat["T"]
1474
+ elif kind == "dew":
1475
+ sat = self.saturation_temperature(z, p0, kind=kind, t0=t0)
1476
+ w0 = sat["x"]
1477
+ ns0 = len(z) + 2
1478
+ p0 = sat["P"]
1479
+ elif kind == "liquid-liquid":
1480
+ ts, ps, tcs, pcs, xs, ys, kinds = yaeos_c.pt2_phase_envelope(
1481
+ self.id, z, kind=kind, t0=t0, p0=p0, max_points=2
1482
+ )
1483
+ w0 = ys[0, :]
1484
+ ns0 = len(z) + 2
1485
+ t0 = ts[0]
1486
+ p0 = ps[0]
1487
+
1488
+ x_ls, ws, betas, ps, ts, iters, ns = yaeos_c.pt_mp_phase_envelope(
1489
+ id=self.id,
1490
+ np=1,
1491
+ z=z,
1492
+ x_l0=[z],
1493
+ w0=w0,
1494
+ betas0=[1],
1495
+ t0=t0,
1496
+ p0=p0,
1497
+ ns0=ns0,
1498
+ ds0=0.001,
1499
+ beta_w=0,
1500
+ max_points=max_points,
1501
+ stop_pressure=stop_pressure,
1502
+ )
1503
+
1504
+ envelope = PTEnvelope(
1505
+ global_composition=z,
1506
+ main_phases_compositions=x_ls,
1507
+ reference_phase_compositions=ws,
1508
+ main_phases_molar_fractions=betas,
1509
+ pressures=ps,
1510
+ temperatures=ts,
1511
+ iterations=iters,
1512
+ specified_variable=ns,
1513
+ )
1514
+
1515
+ return envelope
1516
+
1517
+ msk = ~np.isnan(ts)
1518
+ msk_cp = ~np.isnan(tcs)
1519
+
1520
+ res = {
1521
+ "T": ts[msk],
1522
+ "P": ps[msk],
1523
+ "Tc": tcs[msk_cp],
1524
+ "Pc": pcs[msk_cp],
1525
+ "x": xs[msk],
1526
+ "y": ys[msk],
1527
+ "kinds": kinds[msk],
1528
+ }
1529
+
1530
+ return res
1531
+
1532
+ def phase_envelope_px(
1533
+ self,
1534
+ z0,
1535
+ zi,
1536
+ temperature,
1537
+ kind="bubble",
1538
+ max_points=500,
1539
+ p0=10.0,
1540
+ a0=1e-2,
1541
+ ns0=None,
1542
+ ds0=1e-5,
1543
+ ):
1544
+ """Two phase envelope calculation (PX).
1545
+
1546
+ Calculation of a phase envelope that starts at a given composition and
1547
+ its related to another composition with some proportion.
1548
+
1549
+ Parameters
1550
+ ----------
1551
+ z0 : array_like
1552
+ Initial global mole fractions
1553
+ zi : array_like
1554
+ Final global mole fractions
1555
+ temperature : float
1556
+ Temperature [K]
1557
+ kind : str, optional
1558
+ Kind of saturation point to start the envelope calculation,
1559
+ defaults to "bubble". Options are
1560
+ - "bubble"
1561
+ - "dew"
1562
+ max_points : int, optional
1563
+ Envelope's maximum points to calculate, by default 500
1564
+ p0 : float, optional
1565
+ Initial guess for pressure [bar] for the saturation point of kind:
1566
+ `kind`, by default 10.0
1567
+ a0 : float, optional
1568
+ Initial molar fraction of composition `zi`, by default 0.001
1569
+ ns0 : int, optional
1570
+ Initial specified variable number, by default None.
1571
+ The the first `n=len(z)` values correspond to the K-values,
1572
+ `len(z)+1` is the main phase molar fraction (ussually one) and
1573
+ the last two values are the pressure and alpha.
1574
+ ds0 : float, optional
1575
+ Step for the first specified variable, by default 0.01
1576
+ """
1577
+ if ns0 is None:
1578
+ ns0 = len(z0) + 3
1579
+
1580
+ zi = np.array(zi)
1581
+ z0 = np.array(z0)
1582
+
1583
+ z = a0 * zi + (1 - a0) * z0
1584
+ sat = self.saturation_pressure(
1585
+ z, temperature=temperature, kind=kind, p0=p0
1586
+ )
1587
+
1588
+ if kind == "dew":
1589
+ w0 = sat["x"]
1590
+ else:
1591
+ w0 = sat["y"]
1592
+ p0 = sat["P"]
1593
+
1594
+ envelope = self.phase_envelope_px_mp(
1595
+ z0,
1596
+ zi,
1597
+ temperature,
1598
+ x_l0=[z],
1599
+ w0=w0,
1600
+ betas0=[1],
1601
+ p0=p0,
1602
+ alpha0=a0,
1603
+ ns0=ns0,
1604
+ ds0=ds0,
1605
+ max_points=max_points,
1606
+ )
1607
+
1608
+ return envelope
1609
+
1610
+ def phase_envelope_tx(
1611
+ self,
1612
+ z0,
1613
+ zi,
1614
+ pressure,
1615
+ kind="bubble",
1616
+ max_points=300,
1617
+ t0=150.0,
1618
+ a0=0.001,
1619
+ ns0=None,
1620
+ ds0=0.1,
1621
+ ):
1622
+ """Two phase envelope calculation (TX).
1623
+
1624
+ Calculation of a phase envelope that starts at a given composition and
1625
+ its related to another composition with some proportion.
1626
+
1627
+ Parameters
1628
+ ----------
1629
+ z0 : array_like
1630
+ Initial global mole fractions
1631
+ zi : array_like
1632
+ Final global mole fractions
1633
+ pressure : float
1634
+ Pressure [K]
1635
+ kind : str, optional
1636
+ Kind of saturation point to start the envelope calculation,
1637
+ defaults to "bubble". Options are
1638
+ - "bubble"
1639
+ - "dew"
1640
+ max_points : int, optional
1641
+ Envelope's maximum points to calculate (P, X), by default 300
1642
+ t0 : float, optional
1643
+ Initial guess for temperature [K] for the saturation point of kind:
1644
+ `kind`, by default 150.0
1645
+ a0 : float, optional
1646
+ Initial molar fraction of composition `zi`, by default 0.001
1647
+ ns0 : int, optional
1648
+ Initial specified variable number, by default None.
1649
+ The the first `n=len(z)` values correspond to the K-values, where
1650
+ the last two values are the temperature and alpha.
1651
+ ds0 : float, optional
1652
+ Step for a, by default 0.1
1653
+ """
1654
+ if ns0 is None:
1655
+ ns0 = len(z0) + 2
1656
+ a, ts, xs, ys, acs, pcs, kinds = yaeos_c.tx2_phase_envelope(
1657
+ self.id,
1658
+ z0=z0,
1659
+ zi=zi,
1660
+ kind=kind,
1661
+ max_points=max_points,
1662
+ t0=t0,
1663
+ a0=a0,
1664
+ p=pressure,
1665
+ ns0=ns0,
1666
+ ds0=ds0,
1667
+ )
1668
+
1669
+ msk = ~np.isnan(ts)
1670
+ msk_cp = ~np.isnan(pcs)
1671
+
1672
+ return {
1673
+ "a": a[msk],
1674
+ "T": ts[msk],
1675
+ "x": xs[msk],
1676
+ "y": ys[msk],
1677
+ "ac": acs[msk_cp],
1678
+ "Pc": pcs[msk_cp],
1679
+ "kind": kinds[msk],
1680
+ }
1681
+
1682
+ def phase_envelope_pt3(
1683
+ self,
1684
+ z,
1685
+ x0,
1686
+ y0,
1687
+ w0,
1688
+ beta0,
1689
+ t0,
1690
+ p0,
1691
+ specified_variable=None,
1692
+ first_step=None,
1693
+ max_points=1000,
1694
+ stop_pressure=2500,
1695
+ ):
1696
+ """
1697
+ Three-phase envelope tracing method.
1698
+
1699
+ Calculation of a three-phase envelope that starts with an estimated
1700
+ compositions, pressure, temperature and phase fractions.
1701
+
1702
+ Parameters
1703
+ ----------
1704
+ z : array_like
1705
+ Global mole fractions
1706
+ x0 : array_like
1707
+ Initial phase x mole fractions
1708
+ y0 : array_like
1709
+ Initial phase y mole fractions
1710
+ w0 : array_like
1711
+ Initial incipient phase w mole fractions
1712
+ beta0 : float
1713
+ Initial phase fraction between x and y
1714
+ t0 : float
1715
+ Initial temperature [K]
1716
+ p0 : float
1717
+ Initial pressure [bar]
1718
+ specified_variable : int, optional
1719
+ Initial specified variable number, by default 2*len(z)+2
1720
+ (temperature). The the first `n=(1,len(z))` values correspond to
1721
+ the K-values between phase x and w, the next `n=(len(z)+1,
1722
+ 2*len(z))` are the K-values between phase y and w. The last three
1723
+ values are pressure, temperature and beta.
1724
+ first_step : float, optional
1725
+ Step for the specified variable, by default 0.1
1726
+ max_points : int, optional
1727
+ Maximum number of points to calculate, by default 1000
1728
+ stop_pressure : float, optional
1729
+ Stop at pressure above stop_pressure [bar], default 2500
1730
+ """
1731
+
1732
+ np = 2
1733
+ if specified_variable is None:
1734
+ specified_variable = 2 * len(z) + np + 2
1735
+
1736
+ if first_step is None:
1737
+ first_step = 0.1
1738
+
1739
+ x_ls, ws, betas, ps, ts, iters, ns = yaeos_c.pt_mp_phase_envelope(
1740
+ id=self.id,
1741
+ np=np,
1742
+ z=z,
1743
+ x_l0=[x0, y0],
1744
+ w0=w0,
1745
+ betas0=[1 - beta0, beta0],
1746
+ t0=t0,
1747
+ p0=p0,
1748
+ ns0=specified_variable,
1749
+ ds0=first_step,
1750
+ beta_w=0,
1751
+ max_points=max_points,
1752
+ stop_pressure=stop_pressure,
1753
+ )
1754
+
1755
+ envelope = PTEnvelope(
1756
+ global_composition=z,
1757
+ main_phases_compositions=x_ls,
1758
+ reference_phase_compositions=ws,
1759
+ main_phases_molar_fractions=betas,
1760
+ pressures=ps,
1761
+ temperatures=ts,
1762
+ iterations=iters,
1763
+ specified_variable=ns,
1764
+ )
1765
+
1766
+ return envelope
1767
+
1768
+ def phase_envelope_px3(
1769
+ self,
1770
+ z0,
1771
+ zi,
1772
+ T,
1773
+ x0,
1774
+ y0,
1775
+ w0,
1776
+ beta0,
1777
+ a0,
1778
+ p0,
1779
+ specified_variable=None,
1780
+ first_step=None,
1781
+ max_points=1000,
1782
+ ):
1783
+ """
1784
+ Three-phase envelope tracing method.
1785
+
1786
+ Calculation of a three-phase envelope that starts with an estimated
1787
+ compositions, pressure, temperature and phase fractions.
1788
+
1789
+ Parameters
1790
+ ----------
1791
+ z0 : array_like
1792
+ Global mole fractions of the original fluid
1793
+ zi : array_like
1794
+ Global mole fractions of the other fluid
1795
+ x0 : array_like
1796
+ Initial phase x mole fractions
1797
+ y0 : array_like
1798
+ Initial phase y mole fractions
1799
+ w0 : array_like
1800
+ Initial incipient phase w mole fractions
1801
+ beta0 : float
1802
+ Initial phase fraction between x and y
1803
+ a0 : float
1804
+ Initial molar fraction of the other fluid
1805
+ p0 : float
1806
+ Initial pressure [bar]
1807
+ specified_variable : int, optional
1808
+ Initial specified variable number, by default 2*len(z)+2
1809
+ (temperature). The the first `n=(1,len(z))` values correspond to
1810
+ the K-values between phase x and w, the next `n=(len(z)+1,
1811
+ 2*len(z))` are the K-values between phase y and w. The last three
1812
+ values are pressure, a and beta.
1813
+ first_step : float, optional
1814
+ Step for the specified variable, by default 0.1
1815
+ max_points : int, optional
1816
+ Maximum number of points to calculate, by default 1000
1817
+ """
1818
+ if specified_variable is None:
1819
+ specified_variable = 2 * len(z0) + 2
1820
+
1821
+ if first_step is None:
1822
+ first_step = 0.1
1823
+
1824
+ x, y, w, p, a, beta = yaeos_c.px3_phase_envelope(
1825
+ id=self.id,
1826
+ z0=z0,
1827
+ zi=zi,
1828
+ t=T,
1829
+ x0=x0,
1830
+ y0=y0,
1831
+ w0=w0,
1832
+ p0=p0,
1833
+ a0=a0,
1834
+ beta0=beta0,
1835
+ ns0=specified_variable,
1836
+ ds0=first_step,
1837
+ max_points=max_points,
1838
+ )
1839
+
1840
+ msk = ~np.isnan(a)
1841
+
1842
+ return {
1843
+ "x": x[msk],
1844
+ "y": y[msk],
1845
+ "w": w[msk],
1846
+ "P": p[msk],
1847
+ "a": a[msk],
1848
+ "beta": beta[msk],
1849
+ }
1850
+
1851
+ def phase_envelope_pt_mp(
1852
+ self,
1853
+ z,
1854
+ x_l0,
1855
+ w0,
1856
+ betas0,
1857
+ p0,
1858
+ t0,
1859
+ ns0,
1860
+ ds0,
1861
+ beta_w=0,
1862
+ max_points=1000,
1863
+ stop_pressure=1000,
1864
+ ):
1865
+ """Multi-phase envelope."""
1866
+ x_l0 = np.array(x_l0)
1867
+
1868
+ number_of_phases = x_l0.shape[0]
1869
+
1870
+ x_ls, ws, betas, ps, ts, iters, ns = yaeos_c.pt_mp_phase_envelope(
1871
+ id=self.id,
1872
+ np=number_of_phases,
1873
+ z=z,
1874
+ x_l0=x_l0,
1875
+ w0=w0,
1876
+ betas0=betas0,
1877
+ t0=t0,
1878
+ p0=p0,
1879
+ ns0=ns0,
1880
+ ds0=ds0,
1881
+ beta_w=beta_w,
1882
+ max_points=max_points,
1883
+ stop_pressure=stop_pressure,
1884
+ )
1885
+
1886
+ envelope = PTEnvelope(
1887
+ global_composition=z,
1888
+ main_phases_compositions=x_ls,
1889
+ reference_phase_compositions=ws,
1890
+ main_phases_molar_fractions=betas,
1891
+ pressures=ps,
1892
+ temperatures=ts,
1893
+ iterations=iters,
1894
+ specified_variable=ns,
1895
+ )
1896
+
1897
+ return envelope
1898
+
1899
+ def phase_envelope_px_mp(
1900
+ self,
1901
+ z0,
1902
+ zi,
1903
+ t,
1904
+ x_l0,
1905
+ w0,
1906
+ betas0,
1907
+ p0,
1908
+ ns0,
1909
+ ds0,
1910
+ alpha0=0,
1911
+ beta_w=0,
1912
+ max_points=1000,
1913
+ ):
1914
+ """Multi-phase PX envelope.
1915
+
1916
+ Calculate a phase envelope with a preselected ammount of phases.
1917
+
1918
+ Parameters
1919
+ ----------
1920
+ z0: float, array_like
1921
+ Original Fluid.
1922
+ zi: float, array_like
1923
+ Other fluid.
1924
+ t: float
1925
+ Temperature [K]
1926
+ x_l0: float, matrix [number of phases, number of components]
1927
+ A matrix where each row is the composition of a main phase.
1928
+ Guess for first point.
1929
+ w0: flot, array_like
1930
+ Composition of the reference (ussually incipient) phase.
1931
+ Guess for first point
1932
+ betas0: float, array_like
1933
+ Molar fraction of each main phase. Guess for first point
1934
+ p0: float
1935
+ Pressure guess for first point [bar]
1936
+ ns0: int
1937
+ Initial variable to specifiy.
1938
+ From 1 to `(number_of_phases*number_of_components)` corresponds to
1939
+ each composition.
1940
+ From `number_of_phases*number_of_components` to
1941
+ `number_of_phases*number_of_components + number_of_phases`
1942
+ corresponds to each beta value of the main phases.
1943
+ The last two posibilities are the pressure and molar relation
1944
+ between the two fluids, respectively.
1945
+ """
1946
+ x_l0 = np.array(x_l0, order="F")
1947
+
1948
+ number_of_phases = x_l0.shape[0]
1949
+
1950
+ x_ls, ws, betas, ps, alphas, iters, ns = yaeos_c.px_mp_phase_envelope(
1951
+ id=self.id,
1952
+ np=number_of_phases,
1953
+ z0=z0,
1954
+ zi=zi,
1955
+ x_l0=x_l0,
1956
+ w0=w0,
1957
+ betas0=betas0,
1958
+ t=t,
1959
+ alpha0=alpha0,
1960
+ p0=p0,
1961
+ ns0=ns0,
1962
+ ds0=ds0,
1963
+ beta_w=beta_w,
1964
+ max_points=max_points,
1965
+ )
1966
+
1967
+ return PXEnvelope(
1968
+ temperature=t,
1969
+ global_composition_0=z0,
1970
+ global_composition_i=zi,
1971
+ main_phases_compositions=x_ls,
1972
+ reference_phase_compositions=ws,
1973
+ main_phases_molar_fractions=betas,
1974
+ pressures=ps,
1975
+ alphas=alphas,
1976
+ iterations=iters,
1977
+ specified_variable=ns,
1978
+ )
1979
+
1980
+ def phase_envelope_tx_mp(
1981
+ self,
1982
+ z0,
1983
+ zi,
1984
+ p,
1985
+ x_l0,
1986
+ w0,
1987
+ betas0,
1988
+ t0,
1989
+ ns0,
1990
+ ds0,
1991
+ alpha0=0,
1992
+ beta_w=0,
1993
+ max_points=1000,
1994
+ ):
1995
+ """Multi-phase envelope."""
1996
+
1997
+ number_of_phases = x_l0.shape[0]
1998
+
1999
+ x_ls, ws, betas, ts, alphas, iters, ns = yaeos_c.tx_mp_phase_envelope(
2000
+ id=self.id,
2001
+ np=number_of_phases,
2002
+ z0=z0,
2003
+ zi=zi,
2004
+ x_l0=x_l0,
2005
+ w0=w0,
2006
+ betas0=betas0,
2007
+ p=p,
2008
+ alpha0=alpha0,
2009
+ t0=t0,
2010
+ ns0=ns0,
2011
+ ds0=ds0,
2012
+ beta_w=beta_w,
2013
+ max_points=max_points,
2014
+ )
2015
+
2016
+ return TXEnvelope(
2017
+ pressure=p,
2018
+ global_composition_0=z0,
2019
+ global_composition_i=zi,
2020
+ main_phases_compositions=x_ls,
2021
+ reference_phase_compositions=ws,
2022
+ main_phases_molar_fractions=betas,
2023
+ temperatures=ts,
2024
+ alphas=alphas,
2025
+ iterations=iters,
2026
+ specified_variable=ns,
2027
+ )
2028
+
2029
+ def isopleth(
2030
+ self,
2031
+ z,
2032
+ three_phase=True,
2033
+ dew_start=(500, 0.01),
2034
+ bubble_start=(200, 10),
2035
+ max_points=1000,
2036
+ delta_dew_2ph=0.01,
2037
+ delta_bub_2ph=0.01,
2038
+ delta_dsp_3ph=0.01,
2039
+ stop_pressure=2500,
2040
+ ):
2041
+
2042
+ dew_point = self.saturation_temperature(
2043
+ z, pressure=dew_start[1], kind="dew", t0=dew_start[0]
2044
+ )
2045
+ bub_point = self.saturation_pressure(
2046
+ z, temperature=bubble_start[0], kind="bubble", p0=bubble_start[1]
2047
+ )
2048
+
2049
+ dew_line = self.phase_envelope_pt_mp(
2050
+ z=z,
2051
+ x_l0=[z],
2052
+ w0=dew_point["x"],
2053
+ betas0=[1],
2054
+ p0=dew_point["P"],
2055
+ t0=dew_point["T"],
2056
+ ns0=len(z) + 2,
2057
+ ds0=delta_dew_2ph,
2058
+ max_points=max_points,
2059
+ stop_pressure=stop_pressure,
2060
+ )
2061
+
2062
+ bub_line = self.phase_envelope_pt_mp(
2063
+ z=z,
2064
+ x_l0=[z],
2065
+ w0=bub_point["y"],
2066
+ betas0=[1],
2067
+ p0=bub_point["P"],
2068
+ t0=bub_point["T"],
2069
+ ns0=len(z) + 2,
2070
+ ds0=delta_bub_2ph,
2071
+ max_points=max_points,
2072
+ stop_pressure=stop_pressure,
2073
+ )
2074
+
2075
+ liq = self.phase_envelope_pt(
2076
+ z, kind="liquid-liquid", t0=500, p0=2000, max_points=2
2077
+ )
2078
+
2079
+ if len(liq["T"]) > 0:
2080
+ liq_line = self.phase_envelope_pt_mp(
2081
+ z=z,
2082
+ x_l0=[z],
2083
+ w0=liq.reference_phase_compositions[0],
2084
+ betas0=[1],
2085
+ p0=liq["P"][0],
2086
+ t0=liq["T"][0],
2087
+ ns0=len(z) + 2,
2088
+ ds0=-0.01,
2089
+ max_points=max_points,
2090
+ stop_pressure=1e10,
2091
+ )
2092
+ else:
2093
+ liq_line = None
2094
+
2095
+ dsps_db = intersection(
2096
+ dew_line["T"],
2097
+ dew_line["P"],
2098
+ bub_line["T"],
2099
+ bub_line["P"],
2100
+ )
2101
+
2102
+ if liq_line:
2103
+ dsps_dl = intersection(
2104
+ dew_line["T"],
2105
+ dew_line["P"],
2106
+ liq_line["T"],
2107
+ liq_line["P"],
2108
+ )
2109
+
2110
+ dsps_bl = intersection(
2111
+ bub_line["T"],
2112
+ bub_line["P"],
2113
+ liq_line["T"],
2114
+ liq_line["P"],
2115
+ )
2116
+
2117
+ dsps_set = {
2118
+ "dl": [dsps_dl, dew_line, liq_line],
2119
+ "db": [dsps_db, dew_line, bub_line],
2120
+ "bl": [dsps_bl, bub_line, liq_line],
2121
+ }
2122
+ else:
2123
+ dsps_set = {"db": [dsps_db, dew_line, bub_line]}
2124
+
2125
+ dew_locs = []
2126
+ bub_locs = []
2127
+ liq_locs = []
2128
+
2129
+ three_phase_envs = []
2130
+ stable_lines = {"3ph": [], "2ph": []}
2131
+
2132
+ if three_phase:
2133
+ dew_line_stable = dew_line
2134
+ bub_line_stable = bub_line
2135
+ liq_line_stable = liq_line
2136
+
2137
+ for dsp_name in dsps_set:
2138
+ # Order the DSPs wrt temperature
2139
+ dsps = dsps_set[dsp_name][0]
2140
+ env_1, env_2 = dsps_set[dsp_name][1], dsps_set[dsp_name][2]
2141
+
2142
+ idx = dsps[0].argsort()
2143
+
2144
+ dsps = (dsps[0][idx], dsps[1][idx])
2145
+
2146
+ if 0 < len(dsps[0]) <= 2:
2147
+ for temperature, pressure in zip(dsps[0], dsps[1]):
2148
+ env_1_loc = np.argmin(
2149
+ np.abs(env_1["T"] - temperature)
2150
+ + np.abs(env_1["P"] - pressure)
2151
+ )
2152
+ env_2_loc = np.argmin(
2153
+ np.abs(env_2["T"] - temperature)
2154
+ + np.abs(env_2["P"] - pressure)
2155
+ )
2156
+
2157
+ dew_locs.append(env_1_loc)
2158
+ bub_locs.append(env_2_loc)
2159
+
2160
+ w_env_1 = env_1.reference_phase_compositions[env_1_loc]
2161
+ w_env_2 = env_2.reference_phase_compositions[env_2_loc]
2162
+
2163
+ env1 = self.phase_envelope_pt_mp(
2164
+ z,
2165
+ x_l0=np.array([z, w_env_1]),
2166
+ w0=w_env_2,
2167
+ betas0=[1, 0],
2168
+ t0=temperature,
2169
+ p0=pressure,
2170
+ ns0=2 * len(z) + 2,
2171
+ ds0=delta_dsp_3ph,
2172
+ max_points=1000,
2173
+ stop_pressure=max([pressure * 2, stop_pressure]),
2174
+ )
2175
+
2176
+ env2 = self.phase_envelope_pt_mp(
2177
+ z,
2178
+ x_l0=np.array([z, w_env_2]),
2179
+ w0=w_env_1,
2180
+ betas0=[1, 0],
2181
+ t0=temperature,
2182
+ p0=pressure,
2183
+ ns0=2 * len(z) + 2,
2184
+ ds0=delta_dsp_3ph,
2185
+ max_points=1000,
2186
+ stop_pressure=max([pressure * 2, stop_pressure]),
2187
+ )
2188
+
2189
+ three_phase_envs.append((env1, env2))
2190
+ # if len(dew_locs) == 2:
2191
+ # msk = np.array([False] * len(dew_line))
2192
+ # msk[: dew_locs[1] + 1] = True
2193
+ # msk[dew_locs[0] :] = True
2194
+ # dew_line_stable *= np.nan
2195
+ # dew_line_stable[msk] = dew_line[msk]
2196
+
2197
+ # msk = np.array([False] * len(bub_line))
2198
+ # msk[bub_locs[0] : bub_locs[1] + 1] = True
2199
+ # bub_line_stable *= np.nan
2200
+ # bub_line_stable[msk] = bub_line[msk]
2201
+
2202
+ elif len(dsps[0]) == 0:
2203
+ bub_line_stable *= np.nan
2204
+
2205
+ k0 = dew_line.reference_phase_compositions[0, :] / z
2206
+ flash = self.flash_pt(
2207
+ z,
2208
+ pressure=bub_line["P"][0],
2209
+ temperature=bub_line["T"][0],
2210
+ k0=k0,
2211
+ )
2212
+
2213
+ x_l0 = [z, flash["y"]]
2214
+ w0 = bub_line.reference_phase_compositions[0, :]
2215
+ betas = [1 - flash["beta"], flash["beta"]]
2216
+
2217
+ bubble_isolated = self.phase_envelope_pt_mp(
2218
+ z=z,
2219
+ x_l0=x_l0,
2220
+ w0=w0,
2221
+ betas0=betas,
2222
+ p0=flash["P"],
2223
+ t0=flash["T"],
2224
+ ns0=2 * len(z) + 4,
2225
+ ds0=delta_bub_2ph,
2226
+ max_points=max_points,
2227
+ )
2228
+
2229
+ x_l0 = [z, bub_line.reference_phase_compositions[0, :]]
2230
+ w0 = flash["y"]
2231
+ idx = np.argmax(w0)
2232
+
2233
+ flash = self.flash_pt(z, flash["P"], flash["T"])
2234
+ betas = [1 - flash["beta"], flash["beta"]]
2235
+
2236
+ dew_isolated = self.phase_envelope_pt_mp(
2237
+ z=z,
2238
+ x_l0=x_l0,
2239
+ w0=w0,
2240
+ betas0=betas,
2241
+ p0=flash["P"] - 5,
2242
+ t0=flash["T"] + 5,
2243
+ ns0=2 * len(z) + 3,
2244
+ ds0=delta_bub_2ph,
2245
+ max_points=max_points,
2246
+ )
2247
+
2248
+ three_phase_envs.append((bubble_isolated, dew_isolated))
2249
+
2250
+ stable_lines["2ph"] = {
2251
+ "dew": dew_line_stable,
2252
+ "bub": bub_line_stable,
2253
+ "liq": liq_line_stable,
2254
+ }
2255
+
2256
+ return {
2257
+ "2ph": (dew_line, bub_line, liq_line),
2258
+ "DSP": dsps,
2259
+ "3ph": three_phase_envs,
2260
+ "2ph_stable": stable_lines["2ph"],
2261
+ }
2262
+ else:
2263
+ return {
2264
+ "2ph": {"dew": dew_line, "bub": bub_line},
2265
+ }
2266
+
2267
+ # =========================================================================
2268
+ # Stability analysis
2269
+ # -------------------------------------------------------------------------
2270
+ def stability_analysis(self, z, pressure, temperature):
2271
+ """Perform stability analysis.
2272
+
2273
+ Find all the possible minima values that the :math:`tm` function,
2274
+ defined by Michelsen and Mollerup.
2275
+
2276
+ Parameters
2277
+ ----------
2278
+ z : array_like
2279
+ Global mole fractions
2280
+ pressure : float
2281
+ Pressure [bar]
2282
+ temperature : float
2283
+ Temperature [K]
2284
+
2285
+ Returns
2286
+ -------
2287
+ dict
2288
+ Stability analysis result dictionary with keys:
2289
+ - w: value of the test phase that minimizes the :math:`tm` function
2290
+ - tm: minimum value of the :math:`tm` function.
2291
+ dict
2292
+ All found minimum values of the :math:`tm` function and the
2293
+ corresponding test phase mole fractions.
2294
+ - w: all values of :math:`w` that minimize the :math:`tm` function
2295
+ - tm: all values found minima of the :math:`tm` function
2296
+ """
2297
+ (w_min, tm_min, all_mins) = yaeos_c.stability_zpt(
2298
+ id=self.id, z=z, p=pressure, t=temperature
2299
+ )
2300
+
2301
+ all_mins_w = all_mins[:, : len(z)]
2302
+ all_mins = all_mins[:, -1]
2303
+
2304
+ return {"w": w_min, "tm": tm_min}, {"tm": all_mins, "w": all_mins_w}
2305
+
2306
+ def stability_tm(self, z, w, pressure, temperature):
2307
+ """Calculate the :math:`tm` function.
2308
+
2309
+ Calculate the :math:`tm` function, defined by Michelsen and Mollerup.
2310
+ If this value is negative, it means that the feed with composition `z`
2311
+ is unstable.
2312
+
2313
+ Parameters
2314
+ ----------
2315
+ z : array_like
2316
+ Global mole fractions
2317
+ w : array_like
2318
+ Test Phase mole fractions
2319
+ pressure : float
2320
+ Pressure [bar]
2321
+ temperature : float
2322
+ Temperature [K]
2323
+
2324
+ Returns
2325
+ -------
2326
+ float
2327
+ Value of the :math:`tm` function
2328
+ """
2329
+ return yaeos_c.tm(id=self.id, z=z, w=w, p=pressure, t=temperature)
2330
+
2331
+ # =========================================================================
2332
+ # Critical points and lines
2333
+ # -------------------------------------------------------------------------
2334
+ def critical_point(self, z0, zi=[0, 0], ns=1, s=0, max_iters=100) -> dict:
2335
+ """Critical point calculation.
2336
+
2337
+ Calculate the critical point of a mixture. At a given composition.
2338
+
2339
+ Parameters
2340
+ ----------
2341
+ z0: array_like
2342
+ Mole fractions of original fluid
2343
+ zi: array_like
2344
+ Mole fractinos of other fluid
2345
+ ns: int
2346
+ Number of specification
2347
+ S: float
2348
+ Specification value
2349
+ max_iters: int, optional
2350
+
2351
+ Returns
2352
+ -------
2353
+ dict
2354
+ Critical point calculation result dictionary with keys:
2355
+ - Tc: critical temperature [K]
2356
+ - Pc: critical pressure [bar]
2357
+ - Vc: critical volume [L]
2358
+ """
2359
+ *x, t, p, v = yaeos_c.critical_point(
2360
+ self.id, z0=z0, zi=zi, spec=ns, s=s, max_iters=max_iters
2361
+ )
2362
+
2363
+ return {"x": x, "Tc": t, "Pc": p, "Vc": v}
2364
+
2365
+ def critical_line(
2366
+ self,
2367
+ z0,
2368
+ zi,
2369
+ ns=1,
2370
+ s=1e-5,
2371
+ ds0=1e-2,
2372
+ a0=1e-5,
2373
+ v0=0,
2374
+ t0=0,
2375
+ p0=0,
2376
+ stability_analysis=False,
2377
+ max_points=1000,
2378
+ stop_pressure=2500,
2379
+ ):
2380
+ """Critical Line calculation.
2381
+
2382
+ Calculate the critical line between two compositions
2383
+
2384
+ Parameters
2385
+ ----------
2386
+ z0: array_like
2387
+ Initial global mole fractions
2388
+ zi: array_like
2389
+ Final global mole fractions
2390
+ ns: int, optional
2391
+ Specified variable number, by default 1
2392
+ s: float, optional
2393
+ Specified value, by default 1e-5
2394
+ ds0: float, optional
2395
+ Step for molar fraction of composition `i`
2396
+ a0: float, optional
2397
+ Initial molar fraction of composition `i`
2398
+ v0: float, optional
2399
+ Initial guess for volume [L/mol]
2400
+ t0: float, optional
2401
+ Initial guess for temperature [K]
2402
+ p0: float, optional
2403
+ Initial guess for pressure [bar]
2404
+ max_points: int, optional
2405
+ Maximum number of points to calculate
2406
+ stop_pressure: float, optional
2407
+ Stop when reaching this pressure value
2408
+ """
2409
+ alphas, vs, ts, ps, *cep = yaeos_c.critical_line(
2410
+ self.id,
2411
+ ns=ns,
2412
+ ds0=ds0,
2413
+ a0=a0,
2414
+ v0=v0,
2415
+ t0=t0,
2416
+ p0=p0,
2417
+ s=s,
2418
+ stability_analysis=stability_analysis,
2419
+ z0=z0,
2420
+ zi=zi,
2421
+ max_points=max_points,
2422
+ stop_pressure=stop_pressure,
2423
+ )
2424
+
2425
+ msk = ~np.isnan(ts)
2426
+
2427
+ if stability_analysis:
2428
+ return {
2429
+ "a": alphas[msk],
2430
+ "T": ts[msk],
2431
+ "P": ps[msk],
2432
+ "V": vs[msk],
2433
+ }, {
2434
+ "x": cep[0],
2435
+ "y": cep[1],
2436
+ "P": cep[2],
2437
+ "Vx": cep[3],
2438
+ "Vy": cep[4],
2439
+ "T": cep[5],
2440
+ }
2441
+ else:
2442
+ return {
2443
+ "a": alphas[msk],
2444
+ "T": ts[msk],
2445
+ "P": ps[msk],
2446
+ "V": vs[msk],
2447
+ }
2448
+
2449
+ def critical_line_liquid_liquid(
2450
+ self, z0=[1, 0], zi=[1, 0], pressure=2000, t0=500
2451
+ ):
2452
+ """Find the start of the Liquid-Liquid critical line of a binary.
2453
+
2454
+ Parameters
2455
+ ----------
2456
+ z0: array_like
2457
+ Initial global mole fractions
2458
+ zi: array_like
2459
+ Final global mole fractions
2460
+ pressure: float
2461
+ Pressure [bar]
2462
+ t0: float
2463
+ Initial guess for temperature [K]
2464
+ """
2465
+ a, t, v = yaeos_c.find_llcl(
2466
+ self.id, z0=z0, zi=zi, p=pressure, tstart=t0
2467
+ )
2468
+
2469
+ return a, t, v
2470
+
2471
+ def __del__(self) -> None:
2472
+ """Delete the model from the available models list (Fortran side)."""
2473
+ yaeos_c.make_available_ar_models_list(self.id)