cosmopharm 0.0.31__py3-none-any.whl → 0.1.0a1__py3-none-any.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.
- cosmopharm/__init__.py +2 -3
- cosmopharm/actmodels/actmodel.py +107 -118
- cosmopharm/actmodels/cosmo.py +154 -152
- cosmopharm/components.py +31 -31
- cosmopharm/equilibrium/__init__.py +2 -2
- cosmopharm/equilibrium/lle.py +126 -178
- cosmopharm/equilibrium/sle.py +244 -244
- cosmopharm/utils/__init__.py +3 -3
- cosmopharm/utils/convert.py +58 -58
- cosmopharm/utils/helpers.py +36 -36
- cosmopharm/utils/lle_scanner.py +188 -188
- cosmopharm/utils/spacing.py +246 -246
- {cosmopharm-0.0.31.dist-info → cosmopharm-0.1.0a1.dist-info}/LICENSE +20 -20
- {cosmopharm-0.0.31.dist-info → cosmopharm-0.1.0a1.dist-info}/METADATA +150 -150
- cosmopharm-0.1.0a1.dist-info/RECORD +18 -0
- {cosmopharm-0.0.31.dist-info → cosmopharm-0.1.0a1.dist-info}/WHEEL +1 -1
- cosmopharm/version.py +0 -16
- cosmopharm-0.0.31.dist-info/RECORD +0 -19
- {cosmopharm-0.0.31.dist-info → cosmopharm-0.1.0a1.dist-info}/top_level.txt +0 -0
cosmopharm/equilibrium/lle.py
CHANGED
@@ -1,178 +1,126 @@
|
|
1
|
-
import numpy as np
|
2
|
-
import pandas as pd
|
3
|
-
from scipy.optimize import least_squares, root
|
4
|
-
from typing import Union, Optional, Type, List
|
5
|
-
|
6
|
-
from ..components import Component
|
7
|
-
from ..actmodels import ActModel
|
8
|
-
from ..utils.spacing import spacing
|
9
|
-
from ..utils.lle_scanner import estimate_lle_from_gmix
|
10
|
-
|
11
|
-
|
12
|
-
class LLE:
|
13
|
-
def __init__(self,
|
14
|
-
actmodel: Union[ActModel, Type[ActModel]],
|
15
|
-
mixture: Optional[List[Component]] = None) -> None:
|
16
|
-
self.actmodel = actmodel
|
17
|
-
self.mixture = mixture
|
18
|
-
self._validate_arguments()
|
19
|
-
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
x = np.array([x1, 1-x1])
|
128
|
-
return self.actmodel.thermofac(T, x)
|
129
|
-
|
130
|
-
def binodal(self, T, x0=None):
|
131
|
-
if x0 is None:
|
132
|
-
x0 = [0.1, 0.999]
|
133
|
-
kwargs = dict(bounds=(0,1), ftol=1e-15, xtol=1e-15)
|
134
|
-
res = least_squares(self.fobj_binodal, x0, args=(T,), **kwargs)
|
135
|
-
return res.x
|
136
|
-
|
137
|
-
def spinodal(self, T, x0=None):
|
138
|
-
if x0 is None:
|
139
|
-
x0 = self.binodal(T, x0)
|
140
|
-
kwargs = dict(bounds=(0,1), ftol=1e-15, xtol=1e-15)
|
141
|
-
res = least_squares(self.fobj_spinodal, x0, args=(T,), **kwargs)
|
142
|
-
return res.x
|
143
|
-
|
144
|
-
|
145
|
-
# =============================================================================
|
146
|
-
# AUXILLIARY FUNCTIONS
|
147
|
-
# =============================================================================
|
148
|
-
def approx_init_x0(self, T):
|
149
|
-
x1 = spacing(0,1,51,'poly',n=3)
|
150
|
-
gmix = self.actmodel.gmix(T, x1)
|
151
|
-
xL, xR, yL, yR = estimate_lle_from_gmix(x1, gmix, rough=True)
|
152
|
-
return xL, xR
|
153
|
-
|
154
|
-
def _validate_arguments(self):
|
155
|
-
"""Validate the arguments for the LLE class."""
|
156
|
-
# TODO: Insert case where both actmodel and mixture are provided
|
157
|
-
# (check if acmodel.mixture == mixture, if not raise warning)
|
158
|
-
if isinstance(self.actmodel, ActModel):
|
159
|
-
# If actmodel is an instance of ActModel
|
160
|
-
self.mixture: List[Component] = self.mixture or self.actmodel.mixture
|
161
|
-
elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
|
162
|
-
# If actmodel is a class (subclass of ActModel)
|
163
|
-
if self.mixture is None:
|
164
|
-
raise ValueError("Please provide a valid mixture:Mixture.")
|
165
|
-
self.actmodel: ActModel = self.actmodel(self.mixture)
|
166
|
-
else:
|
167
|
-
# If actmodel is neither an instance nor a subclass of ActModel
|
168
|
-
err = "'actmodel' must be an instance or a subclass of 'ActModel'"
|
169
|
-
raise ValueError(err)
|
170
|
-
|
171
|
-
def is_valid_numpy_array(self, arr: np.ndarray) -> bool:
|
172
|
-
"""Check if a numpy array contains only numbers and no None values."""
|
173
|
-
if not isinstance(arr, np.ndarray):
|
174
|
-
return False
|
175
|
-
if arr.dtype == object: # Check if the array contains objects (which could include None)
|
176
|
-
return not np.any(arr == None)
|
177
|
-
else:
|
178
|
-
return np.issubdtype(arr.dtype, np.number)
|
1
|
+
import numpy as np
|
2
|
+
import pandas as pd
|
3
|
+
from scipy.optimize import least_squares, root
|
4
|
+
from typing import Union, Optional, Type, List
|
5
|
+
|
6
|
+
from ..components import Component
|
7
|
+
from ..actmodels import ActModel
|
8
|
+
from ..utils.spacing import spacing
|
9
|
+
from ..utils.lle_scanner import estimate_lle_from_gmix
|
10
|
+
|
11
|
+
|
12
|
+
class LLE:
|
13
|
+
def __init__(self,
|
14
|
+
actmodel: Union[ActModel, Type[ActModel]],
|
15
|
+
mixture: Optional[List[Component]] = None) -> None:
|
16
|
+
self.actmodel = actmodel
|
17
|
+
self.mixture = mixture
|
18
|
+
self._validate_arguments()
|
19
|
+
|
20
|
+
def fobj_binodal(self, x1, T):
|
21
|
+
# Equilibrium: Isoactivity criterion (aL1 - aL2 = 0)
|
22
|
+
x = np.array([x1, 1-x1])
|
23
|
+
activity = self.actmodel.activity(T, x)
|
24
|
+
equilibrium = np.diff(activity, axis=1)
|
25
|
+
return equilibrium.ravel() # reshape from (2,1) --> (2,)
|
26
|
+
|
27
|
+
def fobj_spinodal(self, x1):
|
28
|
+
T = 0
|
29
|
+
x = np.array([x1, 1-x1])
|
30
|
+
return self.actmodel.thermofac(T, x)
|
31
|
+
|
32
|
+
def binodal(self, T, x0=None, solver='least_squares'):
|
33
|
+
if x0 is None:
|
34
|
+
x0 = [0.1, 0.999] # 1_N2_Ethan
|
35
|
+
|
36
|
+
if solver == 'least_squares':
|
37
|
+
kwargs = dict(bounds=(0,1), ftol=1e-15, xtol=1e-15)
|
38
|
+
res = least_squares(self.fobj_binodal, x0, args=(T,), **kwargs)
|
39
|
+
# print(res.nfev)
|
40
|
+
return res.x, res.nfev
|
41
|
+
else:
|
42
|
+
kwargs = dict(method='krylov', options={'maxiter': 5})
|
43
|
+
res = root(self.fobj_binodal, x0, args=(T,), **kwargs)
|
44
|
+
# print(res.nit)
|
45
|
+
return res.x, 30
|
46
|
+
|
47
|
+
def spinodal(self, x0=None):
|
48
|
+
if x0 is None:
|
49
|
+
x0 = self.binodal()
|
50
|
+
return least_squares(self.fobj_spinodal, x0).x
|
51
|
+
|
52
|
+
# =============================================================================
|
53
|
+
# TODO: (1) Add some "approx_initial_values" function based on gmix
|
54
|
+
# TODO: (2) Overall improve this code to match the SLE code
|
55
|
+
# =============================================================================
|
56
|
+
def approx_init_x0(self, T):
|
57
|
+
x1 = spacing(0,1,51,'poly',n=3)
|
58
|
+
gmix = self.actmodel.gmix(T, x1)
|
59
|
+
xL, xR, yL, yR = estimate_lle_from_gmix(x1, gmix, rough=True)
|
60
|
+
return xL, xR
|
61
|
+
|
62
|
+
def solve_lle(self, T, x0, solver='least_squares', info=True):
|
63
|
+
binodal_x, nfev = self.binodal(T, x0, solver)
|
64
|
+
binodal_w = self.actmodel._convert(binodal_x)
|
65
|
+
formatted_w_binodal = [f"wL{i+1}={value:.4f}" for i, value in enumerate(binodal_w)]
|
66
|
+
formatted_x_binodal = [f"xL{i+1}={value:.6f}" for i, value in enumerate(binodal_x)]
|
67
|
+
msg = ('LLE: ', f"{T=:.2f}", *formatted_w_binodal, *formatted_x_binodal)
|
68
|
+
if info:
|
69
|
+
print(*msg)
|
70
|
+
return binodal_x, binodal_w, nfev
|
71
|
+
return binodal_x, binodal_w, nfev, msg
|
72
|
+
|
73
|
+
def miscibility(self, T, x0=None, max_gap=0.1, max_T=500, dT=25, exponent=2):
|
74
|
+
""" Calculate miscibility """
|
75
|
+
print()
|
76
|
+
print("Calculating LLE...")
|
77
|
+
res = []
|
78
|
+
|
79
|
+
if x0 is None:
|
80
|
+
print("...searching for suitable initial value...")
|
81
|
+
x0 = self.approx_init_x0(T)
|
82
|
+
binodal_x, binodal_w, nfev, msg = self.solve_lle(T, x0, info=False)
|
83
|
+
|
84
|
+
# Check if initial guess is reasonalble - otherwise increase T
|
85
|
+
while binodal_x[0] < x0[0] and T <= max_T:
|
86
|
+
print('LLE: ', f"{T=:.2f}", "...no feasbible initial value found.")
|
87
|
+
T += 10 # Increase T by 10
|
88
|
+
x0 = self.approx_init_x0(T)
|
89
|
+
binodal_x, binodal_w, nfev, msg = self.solve_lle(T, x0, info=False)
|
90
|
+
print("Suitable initial value found! Proceed with calculating LLE...")
|
91
|
+
print(*msg)
|
92
|
+
gap = np.diff(binodal_w)[0]
|
93
|
+
res.append((T, *binodal_w, *binodal_x))
|
94
|
+
|
95
|
+
while gap > max_gap and T <= max_T:
|
96
|
+
solver = 'least_squares' if nfev <= 30 else 'root'
|
97
|
+
solver = 'least_squares'
|
98
|
+
# print(solver)
|
99
|
+
T += dT * gap**exponent
|
100
|
+
x0 = binodal_x
|
101
|
+
binodal_x, binodal_w, nfev = self.solve_lle(T, x0, solver)
|
102
|
+
gap = np.diff(binodal_w)[0]
|
103
|
+
res.append((T, *binodal_w, *binodal_x))
|
104
|
+
|
105
|
+
columns = ['T', 'wL1', 'wL2', 'xL1', 'xL2']
|
106
|
+
res = pd.DataFrame(res, columns=columns)
|
107
|
+
return res
|
108
|
+
|
109
|
+
# =============================================================================
|
110
|
+
# AUXILLIARY FUNCTIONS
|
111
|
+
# =============================================================================
|
112
|
+
def _validate_arguments(self):
|
113
|
+
"""Validate the arguments for the LLE class."""
|
114
|
+
# TODO: Insert case where both actmodel and mixture are provided (check if acmodel.mixture == mixture, if not raise warning)
|
115
|
+
if isinstance(self.actmodel, ActModel):
|
116
|
+
# If actmodel is an instance of ActModel
|
117
|
+
self.mixture: List[Component] = self.mixture or self.actmodel.mixture
|
118
|
+
elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
|
119
|
+
# If actmodel is a class (subclass of ActModel)
|
120
|
+
if self.mixture is None:
|
121
|
+
raise ValueError("Please provide a valid mixture:Mixture.")
|
122
|
+
self.actmodel: ActModel = self.actmodel(self.mixture)
|
123
|
+
else:
|
124
|
+
# If actmodel is neither an instance nor a subclass of ActModel
|
125
|
+
err = "'actmodel' must be an instance or a subclass of 'ActModel'"
|
126
|
+
raise ValueError(err)
|