cosmopharm 0.0.24__tar.gz → 0.0.25__tar.gz

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.
Files changed (24) hide show
  1. {cosmopharm-0.0.24/src/cosmopharm.egg-info → cosmopharm-0.0.25}/PKG-INFO +1 -1
  2. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/README.md +16 -1
  3. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/setup.cfg +1 -1
  4. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/actmodels/actmodel.py +11 -0
  5. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/actmodels/cosmo.py +2 -4
  6. cosmopharm-0.0.25/src/cosmopharm/equilibrium/lle.py +178 -0
  7. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/equilibrium/sle.py +1 -1
  8. {cosmopharm-0.0.24 → cosmopharm-0.0.25/src/cosmopharm.egg-info}/PKG-INFO +1 -1
  9. cosmopharm-0.0.24/src/cosmopharm/equilibrium/lle.py +0 -126
  10. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/LICENSE +0 -0
  11. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/setup.py +0 -0
  12. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/__init__.py +0 -0
  13. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/actmodels/__init__.py +0 -0
  14. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/components.py +0 -0
  15. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/equilibrium/__init__.py +0 -0
  16. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/utils/__init__.py +0 -0
  17. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/utils/convert.py +0 -0
  18. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/utils/helpers.py +0 -0
  19. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/utils/lle_scanner.py +0 -0
  20. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm/utils/spacing.py +0 -0
  21. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm.egg-info/SOURCES.txt +0 -0
  22. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm.egg-info/dependency_links.txt +0 -0
  23. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm.egg-info/requires.txt +0 -0
  24. {cosmopharm-0.0.24 → cosmopharm-0.0.25}/src/cosmopharm.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cosmopharm
3
- Version: 0.0.24
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
@@ -153,6 +153,21 @@ For convenience, here's the citation in BibTeX format:
153
153
  }
154
154
  ```
155
155
 
156
+ ### Gaussian Citation
157
+ **Permission from Gaussian, Inc.** was obtained to make the [.cosmo files](https://github.com/ivanantolo/cosmopharm/tree/main/profiles/polymers/cosmo) for the oligomers included in [our paper in *Molecular Pharmaceutics*](https://dx.doi.org/10.1021/acs.jctc.9b01016) available for academic and research (noncommercial) use. This is to enhance research transparency and facilitate the validation process.
158
+
159
+ When using COSMOPharm with the sigma-profiles provided therein, please also cite Gaussian as follows:
160
+
161
+ ```bibtex
162
+ @misc{g16,
163
+ author={M. J. Frisch and G. W. Trucks and H. B. Schlegel and G. E. Scuseria and M. A. Robb and J. R. Cheeseman and G. Scalmani and V. Barone and G. A. Petersson and H. Nakatsuji and X. Li and M. Caricato and A. V. Marenich and J. Bloino and B. G. Janesko and R. Gomperts and B. Mennucci and H. P. Hratchian and J. V. Ortiz and A. F. Izmaylov and J. L. Sonnenberg and D. Williams-Young and F. Ding and F. Lipparini and F. Egidi and J. Goings and B. Peng and A. Petrone and T. Henderson and D. Ranasinghe and V. G. Zakrzewski and J. Gao and N. Rega and G. Zheng and W. Liang and M. Hada and M. Ehara and K. Toyota and R. Fukuda and J. Hasegawa and M. Ishida and T. Nakajima and Y. Honda and O. Kitao and H. Nakai and T. Vreven and K. Throssell and Montgomery, {Jr.}, J. A. and J. E. Peralta and F. Ogliaro and M. J. Bearpark and J. J. Heyd and E. N. Brothers and K. N. Kudin and V. N. Staroverov and T. A. Keith and R. Kobayashi and J. Normand and K. Raghavachari and A. P. Rendell and J. C. Burant and S. S. Iyengar and J. Tomasi and M. Cossi and J. M. Millam and M. Klene and C. Adamo and R. Cammi and J. W. Ochterski and R. L. Martin and K. Morokuma and O. Farkas and J. B. Foresman and D. J. Fox},
164
+ title={Gaussian˜16 {R}evision {C}.01},
165
+ year={2016},
166
+ note={Gaussian Inc. Wallingford CT}
167
+ }
168
+ ```
169
+
156
170
  ## License
157
171
 
158
- COSMOPharm is released under the MIT License. See the [LICENSE](https://github.com/ivanantolo/cosmopharm/LICENSE) file for more details.
172
+ COSMOPharm is released under the MIT License. See the [LICENSE](https://github.com/ivanantolo/cosmopharm/LICENSE) file for more details.
173
+
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = cosmopharm
3
- version = 0.0.24
3
+ version = 0.0.25
4
4
  author = Ivan Antolovic
5
5
  author_email = Ivan.Antolovic@tu-berlin.de
6
6
  maintainer = Martin Klajmon
@@ -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
  # =============================================================================
@@ -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
- # Handling invalid values for free volume calculation
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])
@@ -0,0 +1,178 @@
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, Dict
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 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
+ # =============================================================================
118
+ def fobj_binodal(self, x1, T):
119
+ # Equilibrium: Isoactivity criterion (aL1 - aL2 = 0)
120
+ x = np.array([x1, 1-x1])
121
+ activity = self.actmodel.activity(T, x)
122
+ equilibrium = np.diff(activity, axis=1)
123
+ return equilibrium.ravel() # reshape from (2,1) --> (2,)
124
+
125
+ def fobj_spinodal(self, x1):
126
+ T = 0
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)
@@ -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"w={res['w']:.4f}", f"x={res['x']:.4f}")
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.24
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
@@ -1,126 +0,0 @@
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)
File without changes
File without changes