cosmopharm 0.0.23.2__py3-none-any.whl → 0.0.25__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/actmodels/actmodel.py +11 -0
- cosmopharm/actmodels/cosmo.py +2 -4
- cosmopharm/equilibrium/lle.py +122 -70
- cosmopharm/equilibrium/sle.py +1 -1
- {cosmopharm-0.0.23.2.dist-info → cosmopharm-0.0.25.dist-info}/METADATA +1 -1
- cosmopharm-0.0.25.dist-info/RECORD +18 -0
- cosmopharm-0.0.23.2.dist-info/RECORD +0 -18
- {cosmopharm-0.0.23.2.dist-info → cosmopharm-0.0.25.dist-info}/LICENSE +0 -0
- {cosmopharm-0.0.23.2.dist-info → cosmopharm-0.0.25.dist-info}/WHEEL +0 -0
- {cosmopharm-0.0.23.2.dist-info → cosmopharm-0.0.25.dist-info}/top_level.txt +0 -0
cosmopharm/actmodels/actmodel.py
CHANGED
@@ -35,6 +35,17 @@ class ActModel:
|
|
35
35
|
# Fill gmix with calculated values where the mask is True
|
36
36
|
gmix[mask] = _gmix
|
37
37
|
return gmix[0] if is_scalar else gmix
|
38
|
+
|
39
|
+
def thermofac(self, T, x):
|
40
|
+
""" Approximate thermodynamic factor
|
41
|
+
Simple derivative form, when no analytical equation is available.
|
42
|
+
"""
|
43
|
+
def f(x1):
|
44
|
+
x = np.array([x1, 1-x1])
|
45
|
+
return self.lngamma(T, x)[0]
|
46
|
+
h, x = 0.0001, x[0]
|
47
|
+
dy = (f(x+h)-f(x-h))/(2*h)
|
48
|
+
return 1 + x * dy
|
38
49
|
|
39
50
|
|
40
51
|
# =============================================================================
|
cosmopharm/actmodels/cosmo.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
import numpy as np
|
3
2
|
import cCOSMO
|
4
3
|
|
@@ -6,9 +5,9 @@ from typing import List, Union, Literal
|
|
6
5
|
from .actmodel import ActModel
|
7
6
|
from ..components import Component
|
8
7
|
|
9
|
-
|
10
8
|
class COSMOSAC(ActModel):
|
11
|
-
|
9
|
+
|
10
|
+
# Handle invalid values for free volume calculation
|
12
11
|
class InvalidFreeVolumeParametersException(Exception):
|
13
12
|
pass
|
14
13
|
|
@@ -67,7 +66,6 @@ class COSMOSAC(ActModel):
|
|
67
66
|
(can replace ln_gamma_comb of normal COSMO-SAC) - Kuo2013
|
68
67
|
x, v_298, v_hc are 1D arrays (number of elements = number of components)
|
69
68
|
"""
|
70
|
-
# TODO: Make sure, that v_298 and v_hc are provided, else "FV" not possible
|
71
69
|
self.validate_free_volume_parameters() # Ensure components are valid before proceeding
|
72
70
|
v_298 = np.array([comp.v_298 for comp in self.mixture])
|
73
71
|
v_hc = np.array([comp.v_hc for comp in self.mixture])
|
cosmopharm/equilibrium/lle.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import numpy as np
|
2
2
|
import pandas as pd
|
3
3
|
from scipy.optimize import least_squares, root
|
4
|
-
from typing import Union, Optional, Type, List
|
4
|
+
from typing import Union, Optional, Type, List, Dict
|
5
5
|
|
6
6
|
from ..components import Component
|
7
7
|
from ..actmodels import ActModel
|
@@ -17,6 +17,104 @@ class LLE:
|
|
17
17
|
self.mixture = mixture
|
18
18
|
self._validate_arguments()
|
19
19
|
|
20
|
+
def miscibility(self,
|
21
|
+
T: float,
|
22
|
+
x0: np.ndarray = None,
|
23
|
+
x0_type: str = 'mole',
|
24
|
+
max_gap: float = 0.1,
|
25
|
+
max_gap_type: str = 'mole',
|
26
|
+
max_T: float = 1000,
|
27
|
+
dT: float = 10,
|
28
|
+
exponent: float = 1
|
29
|
+
) -> pd.DataFrame:
|
30
|
+
""" Calculate miscibility """
|
31
|
+
self.config = getattr(self.actmodel, 'config', '')
|
32
|
+
Mw = np.array([c.Mw for c in self.mixture])
|
33
|
+
self.is_valid_Mw = self.is_valid_numpy_array(Mw)
|
34
|
+
res = {'binodal':[], 'spinodal':[]}
|
35
|
+
var = 'x' if max_gap_type == 'mole' else 'w'
|
36
|
+
print()
|
37
|
+
print("Calculating LLE...")
|
38
|
+
|
39
|
+
# Define column names
|
40
|
+
binodal_columns = ['T', 'xL1', 'xL2']
|
41
|
+
if self.is_valid_Mw:
|
42
|
+
binodal_columns += ['wL1', 'wL2']
|
43
|
+
|
44
|
+
# Check for valid molar masses
|
45
|
+
if x0_type == 'weight':
|
46
|
+
if self.is_valid_Mw:
|
47
|
+
x0 = self.convert_to_mole_fractions(x0, self.mixture.Mw)
|
48
|
+
else:
|
49
|
+
raise ValueError("Molar masses are not available for conversion from weight to mole fraction.")
|
50
|
+
# =============================================================================
|
51
|
+
# TODO: Implement all edge cases (no LLE, bad approximation, ....)
|
52
|
+
# TODO: Improve code structure
|
53
|
+
# Approximate initial value for LLE
|
54
|
+
if x0 is None:
|
55
|
+
print("...searching for suitable initial value...")
|
56
|
+
x0 = self.approx_init_x0(T)
|
57
|
+
|
58
|
+
if any(x is None for x in x0):
|
59
|
+
print("...no initial value at T0 was found. Try another T0.")
|
60
|
+
return pd.DataFrame(columns=binodal_columns)
|
61
|
+
|
62
|
+
# Check if initial guess is reasonable - otherwise increase T
|
63
|
+
# TODO: Check whether it might be an LCST if isLower
|
64
|
+
binodal = self.solve_lle(T, x0, show_output=False)
|
65
|
+
isEqual = np.diff(binodal['x'])[0] < 1e-8 # check if both phases have equal composition
|
66
|
+
if isEqual:
|
67
|
+
print("...no initial value at T0 was found. Try another T0.")
|
68
|
+
return pd.DataFrame(columns=binodal_columns)
|
69
|
+
isLower = min(binodal['x']) < min(x0) # lower bound below min(x0)
|
70
|
+
while isLower and T <= max_T:
|
71
|
+
print('LLE: ', f"{T=:.2f}", "...no feasbible initial value found.")
|
72
|
+
T += 10 # Increase T by 10
|
73
|
+
x0 = self.approx_init_x0(T)
|
74
|
+
binodal = self.solve_lle(T, x0, show_output=False)
|
75
|
+
print("Suitable initial value found! Proceed with calculating LLE...")
|
76
|
+
# =============================================================================
|
77
|
+
# First iteration step
|
78
|
+
binodal = self.solve_lle(T, x0)
|
79
|
+
gap = np.diff(binodal[var])[0]
|
80
|
+
res['binodal'].append((T, *[val for vals in binodal.values() for val in vals]))
|
81
|
+
|
82
|
+
# Subsequent iteration steps
|
83
|
+
while gap > max_gap and T <= max_T:
|
84
|
+
T += dT * gap**exponent
|
85
|
+
x0 = binodal['x']
|
86
|
+
binodal = self.solve_lle(T, x0)
|
87
|
+
gap = np.diff(binodal[var])[0]
|
88
|
+
res['binodal'].append((T, *[val for vals in binodal.values() for val in vals]))
|
89
|
+
|
90
|
+
# Convert lists to DataFrames
|
91
|
+
res = pd.DataFrame(res['binodal'], columns=binodal_columns)
|
92
|
+
return res
|
93
|
+
|
94
|
+
# =============================================================================
|
95
|
+
# MATHEMATICS
|
96
|
+
# =============================================================================
|
97
|
+
|
98
|
+
def solve_lle(self, T: float, x0: np.ndarray, show_output=True) -> Dict[str, np.ndarray]:
|
99
|
+
""" Solve for liquid-liquid equilibrium (LLE) at a given temperature and initial composition. """
|
100
|
+
binodal = {'x': self.binodal(T, x0)}
|
101
|
+
output = [f"{k}={v:.4f}" for k,v in zip(['xL1', 'xL2'], binodal['x'])]
|
102
|
+
|
103
|
+
if self.is_valid_Mw:
|
104
|
+
binodal['w'] = self.actmodel._convert(binodal['x'])
|
105
|
+
output += [f"{k}={v:.4f}" for k,v in zip(['wL1', 'wL2'], binodal['w'])]
|
106
|
+
|
107
|
+
if show_output:
|
108
|
+
prefix = f'LLE ({self.config})' if self.config else 'LLE'
|
109
|
+
print(f'{prefix}: ', f"{T=:.2f}", *output)
|
110
|
+
return binodal
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
# =============================================================================
|
116
|
+
# THERMODYNAMICS
|
117
|
+
# =============================================================================
|
20
118
|
def fobj_binodal(self, x1, T):
|
21
119
|
# Equilibrium: Isoactivity criterion (aL1 - aL2 = 0)
|
22
120
|
x = np.array([x1, 1-x1])
|
@@ -29,29 +127,23 @@ class LLE:
|
|
29
127
|
x = np.array([x1, 1-x1])
|
30
128
|
return self.actmodel.thermofac(T, x)
|
31
129
|
|
32
|
-
def binodal(self, T, x0=None
|
130
|
+
def binodal(self, T, x0=None):
|
33
131
|
if x0 is None:
|
34
|
-
x0 = [0.1, 0.999]
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
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
|
46
136
|
|
47
|
-
def spinodal(self, x0=None):
|
137
|
+
def spinodal(self, T, x0=None):
|
48
138
|
if x0 is None:
|
49
|
-
x0 = self.binodal()
|
50
|
-
|
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
|
+
|
51
144
|
|
52
145
|
# =============================================================================
|
53
|
-
#
|
54
|
-
# TODO: (2) Overall improve this code to match the SLE code
|
146
|
+
# AUXILLIARY FUNCTIONS
|
55
147
|
# =============================================================================
|
56
148
|
def approx_init_x0(self, T):
|
57
149
|
x1 = spacing(0,1,51,'poly',n=3)
|
@@ -59,59 +151,10 @@ class LLE:
|
|
59
151
|
xL, xR, yL, yR = estimate_lle_from_gmix(x1, gmix, rough=True)
|
60
152
|
return xL, xR
|
61
153
|
|
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
154
|
def _validate_arguments(self):
|
113
155
|
"""Validate the arguments for the LLE class."""
|
114
|
-
# TODO: Insert case where both actmodel and mixture are provided
|
156
|
+
# TODO: Insert case where both actmodel and mixture are provided
|
157
|
+
# (check if acmodel.mixture == mixture, if not raise warning)
|
115
158
|
if isinstance(self.actmodel, ActModel):
|
116
159
|
# If actmodel is an instance of ActModel
|
117
160
|
self.mixture: List[Component] = self.mixture or self.actmodel.mixture
|
@@ -124,3 +167,12 @@ class LLE:
|
|
124
167
|
# If actmodel is neither an instance nor a subclass of ActModel
|
125
168
|
err = "'actmodel' must be an instance or a subclass of 'ActModel'"
|
126
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)
|
cosmopharm/equilibrium/sle.py
CHANGED
@@ -77,7 +77,7 @@ class SLE:
|
|
77
77
|
x0 = out if not is_iterable else x0
|
78
78
|
res = {key: arg, lock: out, 'vary': self._vary}
|
79
79
|
res['w'] = self.actmodel._convert(res['x'])[0]
|
80
|
-
text = (f"T={res['T']:.2f}", f"
|
80
|
+
text = (f"T={res['T']:.2f}", f"x={res['x']:.4f}", f"w={res['w']:.4f}")
|
81
81
|
if self.show_progress:
|
82
82
|
print(f'SLE ({self.config}): ', *text)
|
83
83
|
yield res
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cosmopharm
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.25
|
4
4
|
Summary: Predictive modeling for drug-polymer compatibility in pharmaceutical formulations using COSMO-SAC.
|
5
5
|
Home-page: https://github.com/ivanantolo/cosmopharm,
|
6
6
|
Author: Ivan Antolovic
|
@@ -0,0 +1,18 @@
|
|
1
|
+
cosmopharm/__init__.py,sha256=sdgLzbqylG8DDAJ5J96YiO4egn9xVJTx2uzaIZ8qj4g,68
|
2
|
+
cosmopharm/components.py,sha256=wEQQ0ZNOrFvG9SdhKAzBgqTbT6DtuoTreNBEdLw8Lm0,981
|
3
|
+
cosmopharm/actmodels/__init__.py,sha256=9iH67yrdSaf10Fj8LwRikUDUMeMxsvUHRPEaWc3384k,59
|
4
|
+
cosmopharm/actmodels/actmodel.py,sha256=rm3-3oWX71IrgdxjcO90bAtlBBsYWOu4cr_oIQH-zZ4,5437
|
5
|
+
cosmopharm/actmodels/cosmo.py,sha256=Dj7kjFYSvJWESG2Y_dP5b6yCCpWYzD5tkh3ATvXIfjg,5873
|
6
|
+
cosmopharm/equilibrium/__init__.py,sha256=5NsIbQEwELjeeoFEiWelnzHnhTzt5zsBh3r5icn_AIQ,44
|
7
|
+
cosmopharm/equilibrium/lle.py,sha256=-xGnJg_7paMPMVaL1pzkF1UNWtnAPSnk1UbMPB11eqM,7814
|
8
|
+
cosmopharm/equilibrium/sle.py,sha256=ScyVq0hNqpWgAbWSKoPARZdO2wf3I9uGzv8L9oDlCtY,11187
|
9
|
+
cosmopharm/utils/__init__.py,sha256=qfUPovmZ9ukj6ZbTfndUOH6EX0ZrzRNjLZEDIVS8UvM,113
|
10
|
+
cosmopharm/utils/convert.py,sha256=V-7jY-Sb7C38N5bQcp1c27EOiVJfriP6zRbLAIKgrdE,2470
|
11
|
+
cosmopharm/utils/helpers.py,sha256=CXUTh3jVStHno_W_Z7o8RvQ6SveSjw_Ss31CkvfROfs,1460
|
12
|
+
cosmopharm/utils/lle_scanner.py,sha256=So9FCxLLcHmBkuF6zggMo3W3gFBocEmuRzyxVGy69JM,6587
|
13
|
+
cosmopharm/utils/spacing.py,sha256=vtM9b4wodpFGkZFGGLhiSXT51Zl6fNK2Og4oRcbLFH4,9222
|
14
|
+
cosmopharm-0.0.25.dist-info/LICENSE,sha256=25ZCycfBgonIECGYnZTy72eJVfzcHCEOz3DM9sTx7do,1162
|
15
|
+
cosmopharm-0.0.25.dist-info/METADATA,sha256=aFK0UFQUT0QnDEPmfMLOl9aWpFBKCa5CDsV32yLcEeQ,7097
|
16
|
+
cosmopharm-0.0.25.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
17
|
+
cosmopharm-0.0.25.dist-info/top_level.txt,sha256=MGniVgvs1yq4sn6HQ7ErDVYV_g3st3Fs8TTFHOJVQ9I,11
|
18
|
+
cosmopharm-0.0.25.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
cosmopharm/__init__.py,sha256=sdgLzbqylG8DDAJ5J96YiO4egn9xVJTx2uzaIZ8qj4g,68
|
2
|
-
cosmopharm/components.py,sha256=wEQQ0ZNOrFvG9SdhKAzBgqTbT6DtuoTreNBEdLw8Lm0,981
|
3
|
-
cosmopharm/actmodels/__init__.py,sha256=9iH67yrdSaf10Fj8LwRikUDUMeMxsvUHRPEaWc3384k,59
|
4
|
-
cosmopharm/actmodels/actmodel.py,sha256=69jluNR7Tb4BHwtkCQLI3NQ_0AEZcTDM69IdRPz9--w,5072
|
5
|
-
cosmopharm/actmodels/cosmo.py,sha256=tpYboI369rEOIkYgGqLyqgQSfKEgxwONULC4ZKDKIHI,5962
|
6
|
-
cosmopharm/equilibrium/__init__.py,sha256=5NsIbQEwELjeeoFEiWelnzHnhTzt5zsBh3r5icn_AIQ,44
|
7
|
-
cosmopharm/equilibrium/lle.py,sha256=Ru0_mso43vZNjy8ybdVQeweAsaZoa_yJiUBljn8qoNU,5472
|
8
|
-
cosmopharm/equilibrium/sle.py,sha256=E89JHAq-0XpJvSf2ybeVoNuV8OH55DHiJL6-8r33ggc,11187
|
9
|
-
cosmopharm/utils/__init__.py,sha256=qfUPovmZ9ukj6ZbTfndUOH6EX0ZrzRNjLZEDIVS8UvM,113
|
10
|
-
cosmopharm/utils/convert.py,sha256=V-7jY-Sb7C38N5bQcp1c27EOiVJfriP6zRbLAIKgrdE,2470
|
11
|
-
cosmopharm/utils/helpers.py,sha256=CXUTh3jVStHno_W_Z7o8RvQ6SveSjw_Ss31CkvfROfs,1460
|
12
|
-
cosmopharm/utils/lle_scanner.py,sha256=So9FCxLLcHmBkuF6zggMo3W3gFBocEmuRzyxVGy69JM,6587
|
13
|
-
cosmopharm/utils/spacing.py,sha256=vtM9b4wodpFGkZFGGLhiSXT51Zl6fNK2Og4oRcbLFH4,9222
|
14
|
-
cosmopharm-0.0.23.2.dist-info/LICENSE,sha256=25ZCycfBgonIECGYnZTy72eJVfzcHCEOz3DM9sTx7do,1162
|
15
|
-
cosmopharm-0.0.23.2.dist-info/METADATA,sha256=T0QDoMBVCsBg3ZEit14sLVc4JmmOdWgfkDl84ZgdFYY,7099
|
16
|
-
cosmopharm-0.0.23.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
17
|
-
cosmopharm-0.0.23.2.dist-info/top_level.txt,sha256=MGniVgvs1yq4sn6HQ7ErDVYV_g3st3Fs8TTFHOJVQ9I,11
|
18
|
-
cosmopharm-0.0.23.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|