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