cosmopharm 0.0.22__tar.gz → 0.0.23__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 (25) hide show
  1. {cosmopharm-0.0.22/src/cosmopharm.egg-info → cosmopharm-0.0.23}/PKG-INFO +4 -4
  2. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/README.md +2 -2
  3. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/setup.cfg +2 -3
  4. cosmopharm-0.0.23/src/cosmopharm/actmodels/actmodel.py +107 -0
  5. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/actmodels/cosmo.py +40 -23
  6. cosmopharm-0.0.23/src/cosmopharm/equilibrium/lle.py +126 -0
  7. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/equilibrium/sle.py +71 -38
  8. {cosmopharm-0.0.22 → cosmopharm-0.0.23/src/cosmopharm.egg-info}/PKG-INFO +4 -4
  9. cosmopharm-0.0.22/src/cosmopharm/actmodels/actmodel.py +0 -83
  10. cosmopharm-0.0.22/src/cosmopharm/equilibrium/lle.py +0 -89
  11. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/LICENSE +0 -0
  12. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/setup.py +0 -0
  13. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/__init__.py +0 -0
  14. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/actmodels/__init__.py +0 -0
  15. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/components.py +0 -0
  16. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/equilibrium/__init__.py +0 -0
  17. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/utils/__init__.py +0 -0
  18. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/utils/convert.py +0 -0
  19. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/utils/helpers.py +0 -0
  20. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/utils/lle_scanner.py +0 -0
  21. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm/utils/spacing.py +0 -0
  22. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm.egg-info/SOURCES.txt +0 -0
  23. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm.egg-info/dependency_links.txt +0 -0
  24. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/src/cosmopharm.egg-info/requires.txt +0 -0
  25. {cosmopharm-0.0.22 → cosmopharm-0.0.23}/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.22
3
+ Version: 0.0.23
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
@@ -38,13 +38,13 @@ Requires-Dist: matplotlib>=3.0; extra == "examples"
38
38
 
39
39
  # COSMOPharm
40
40
 
41
+ Welcome to the COSMOPharm package, accompanying [our paper in *J. Chem. Theory Comput.*](https://dx.doi.org/10.1021/acs.jctc.9b01016). This project and its associated publication offer insights and a practical toolkit for researching drug-polymer and drug-solvent systems, aiming to provide the scientific community with the means to reproduce our findings and further the development of COSMO-SAC-based models.
42
+
41
43
  <p align="center">
42
44
  <!-- <img src="https://github.com/usnistgov/COSMOSAC/raw/master/JCTC2020.PNG" alt="TOC Figure" width="500"> -->
43
45
  <img src="https://github.com/usnistgov/COSMOSAC/raw/master/JCTC2020.PNG" alt="TOC Figure">
44
46
  </p>
45
47
 
46
- Welcome to the COSMOPharm package, accompanying [our paper in *J. Chem. Theory Comput.*](https://dx.doi.org/10.1021/acs.jctc.9b01016). This project and its associated publication offer insights and a practical toolkit for researching drug-polymer and drug-solvent systems, aiming to provide the scientific community with the means to reproduce our findings and further the development of COSMO-SAC-based models.
47
-
48
48
  ## About
49
49
 
50
50
  COSMOPharm is a Python package designed for predictive modeling of drug-polymer compatibility and drug-solubility in common solvents. It leverages the COSMO-SAC (Conductor-like Screening Model Segment Activity Coefficient) model, offering a robust platform for solubility, miscibility, and phase behavior prediction in drug formulation processes.
@@ -62,7 +62,7 @@ Install COSMOPharm with pip:
62
62
 
63
63
  `pip install cosmopharm`
64
64
 
65
- Ensure you have installed the cCOSMO library as per instructions on the [COSMOSAC GitHub page](https://github.com/usnistgov/COSMOSAC).
65
+ Ensure you have installed the `cCOSMO` library as per instructions on the [COSMOSAC GitHub page](https://github.com/usnistgov/COSMOSAC).
66
66
 
67
67
  ## Quick Start
68
68
 
@@ -23,7 +23,7 @@ COSMOPharm is a Python package designed to streamline the predictive modeling of
23
23
  ### Quick Installation
24
24
  For most users, the quickest and easiest way to install COSMOPharm is via pip, which will manage all dependencies for you. Ensure you have already installed the `cCOSMO` library by following the instructions on the [COSMOSAC GitHub page](https://github.com/usnistgov/COSMOSAC).
25
25
 
26
- Once cCOSMO is installed, you can install COSMOPharm directly from [PyPI](https://pypi.org/project/cosmopharm/):
26
+ Once `cCOSMO` is installed, you can install COSMOPharm directly from [PyPI](https://pypi.org/project/cosmopharm/):
27
27
 
28
28
  ```
29
29
  pip install cosmopharm
@@ -65,7 +65,7 @@ python setup.py install
65
65
 
66
66
  While this method is straightforward, using `pip` is generally preferred for its dependency management capabilities.
67
67
 
68
- Please note: Before proceeding with either advanced installation option, ensure the cCOSMO library is installed as described at the beginning of this section.
68
+ Please note: Before proceeding with either advanced installation option, ensure the `cCOSMO` library is installed as described at the beginning of this section.
69
69
 
70
70
  ## Quick Start
71
71
 
@@ -1,12 +1,11 @@
1
1
  [metadata]
2
2
  name = cosmopharm
3
- version = 0.0.22
3
+ version = 0.0.23
4
4
  author = Ivan Antolovic
5
5
  author_email = Ivan.Antolovic@tu-berlin.de
6
6
  maintainer = Martin Klajmon
7
7
  maintainer_email = Martin.Klajmon@vscht.cz
8
- description =
9
- Predictive modeling for drug-polymer compatibility in pharmaceutical formulations using COSMO-SAC.
8
+ description = Predictive modeling for drug-polymer compatibility in pharmaceutical formulations using COSMO-SAC.
10
9
  keywords = Drug-Polymer Compatibility, Amorphous Solid Dispersions, Pharmaceutical Formulation, COSMO-SAC Model, Solubility Prediction, Miscibility Analysis, Phase Behavior Prediction, Pharmaceutical Sciences, Drug Formulation Research, Polymer Science, Predictive Modeling in Pharma, Drug Development Tools, Biopharmaceuticals
11
10
  url = https://github.com/ivanantolo/cosmopharm,
12
11
  license = MIT
@@ -0,0 +1,107 @@
1
+ import numpy as np
2
+ import numbers
3
+ from numpy.typing import NDArray
4
+ from typing import List, Literal
5
+
6
+ from ..components import Component
7
+ from ..utils.convert import convert
8
+
9
+ class ActModel:
10
+
11
+ def __init__(self, components: List[Component]):
12
+ self.mixture = components
13
+
14
+ def lngamma(self, T, x):
15
+ raise NotImplementedError("lngamma() hasn't been implemented yet.")
16
+
17
+ def activity(self, T, x):
18
+ act = np.log(x) + self.lngamma(T, x)
19
+ act[(act == np.inf) | (act == -np.inf)] = np.nan
20
+ return act
21
+
22
+ def gmix(self, T, x):
23
+ is_scalar = np.isscalar(x)
24
+ # Convert input as needed
25
+ x = self._convert_input(x)
26
+ # Create mask to identify columns that don't contain 0 or 1
27
+ mask = np.any((x != 0) & (x != 1), axis=0)
28
+ # Apply the mask to filter x
29
+ _x = x[:, mask]
30
+ # Calculate gmix for the x values
31
+ _gmix = _x * (np.log(_x) + self.lngamma(T, _x))
32
+ _gmix = np.sum(_gmix, axis=0)
33
+ # Initialize gmix array with zeros
34
+ gmix = np.zeros(1 if x.ndim==1 else x.shape[1])
35
+ # Fill gmix with calculated values where the mask is True
36
+ gmix[mask] = _gmix
37
+ return gmix[0] if is_scalar else gmix
38
+
39
+
40
+ # =============================================================================
41
+ # Wrapper functions (Decorators)
42
+ # =============================================================================
43
+ @staticmethod
44
+ def vectorize(func):
45
+ ''' Intended vor ActModels where only single mole fractions can be
46
+ handled, like e.g. COSMO-SAC. This function vectorizes the lngamma()
47
+ to make it work with arrays of mole fractions.
48
+ '''
49
+ def wrapper(self, T, x):
50
+ # Convert input to appropriate format
51
+ x = self._convert_input(x)
52
+ # Process based on the dimensionality of x
53
+ if x.ndim == 1:
54
+ return func(self, T, x)
55
+ elif x.ndim == 2:
56
+ results = [func(self, T, x[:, col]) for col in range(x.shape[1])]
57
+ return np.array(results).T
58
+ else:
59
+ raise ValueError("Input must be either a scalar, 0D, 1D or 2D array")
60
+ return wrapper
61
+
62
+
63
+ # =============================================================================
64
+ # Auxilliary functions
65
+ # =============================================================================
66
+ def _convert_input(self, x):
67
+ """Converts input to a 1-dim ndarray if it's a number or 0-dim ndarray."""
68
+ if isinstance(x, numbers.Number) or (isinstance(x, np.ndarray) and x.ndim == 0):
69
+ return np.array([float(x), 1 - float(x)])
70
+ elif isinstance(x, np.ndarray) and x.ndim == 1 and len(x) != len(self.mixture):
71
+ return np.array([x, 1 - x])
72
+ return x
73
+
74
+ def _convert(self,
75
+ x : NDArray[np.float64],
76
+ to : Literal['weight', 'mole'] ='weight'
77
+ ) -> NDArray[np.float64]:
78
+ """
79
+ Convert the fraction of a binary mixture between mole fraction and weight fraction.
80
+
81
+ This method is designed for internal use with binary mixtures, where the mixture is defined by two components.
82
+ It uses the 'convert' function to perform the conversion by creating an array with the fractions of both
83
+ components and the molecular weights from the mixture's attributes.
84
+
85
+ Parameters:
86
+ x (NDArray[np.float64]): The mole or weight fraction of the first component of the mixture.
87
+ If converting 'to' weight, 'x' represents mole fractions; if converting 'to' mole,
88
+ 'x' represents weight fractions. This should be a single value or a 1D array of values.
89
+ to (Literal['weight', 'mole'], optional): The target type for the conversion. Defaults to 'weight'.
90
+ Use 'weight' to convert mole fractions to weight fractions,
91
+ and 'mole' to convert weight fractions to mole fractions.
92
+
93
+ Returns:
94
+ NDArray[np.float64]: The converted fraction(s) of the first component in the same shape as 'x'.
95
+ If 'x' is a single value, the return will be a single converted value;
96
+ if 'x' is a 1D array, the return will be a 1D array of converted values.
97
+
98
+ Example:
99
+ >>> mixture = Mixture(components=[component1, component2], Mw=np.array([18.01528, 46.06844]))
100
+ >>> sle = SLE(mix=mixture)
101
+ >>> x_mole_fraction = np.array([0.4]) # Mole fraction of the first component
102
+ >>> x_weight_fraction = sle._convert(x_mole_fraction, to='weight')
103
+ >>> print(x_weight_fraction)
104
+ array([0.01373165])
105
+ """
106
+ Mw = np.array([c.Mw for c in self.mixture])
107
+ return convert(x=np.array([x, 1-x], dtype=np.float64), Mw=Mw, to=to)[0]
@@ -1,17 +1,25 @@
1
+
1
2
  import numpy as np
3
+ import cCOSMO
4
+
5
+ from typing import List, Union, Literal
2
6
  from .actmodel import ActModel
7
+ from ..components import Component
3
8
 
4
9
  class COSMOSAC(ActModel):
5
- def __init__(self, COSMO, components: list, free_volume=False,
6
- dispersion=False, combinatorial=True):
10
+ def __init__(self,
11
+ COSMO: Union[cCOSMO.COSMO1, cCOSMO.COSMO3],
12
+ mixture: List[Component],
13
+ combinatorial: Union[Literal['sg', 'fv'], bool] = 'sg',
14
+ dispersion: bool = False,
15
+ ) -> None:
7
16
  self.COSMO = COSMO
8
- self.mix = components
17
+ self.mixture = mixture
9
18
  # Flexible assignment of 'get_lngamma_comb' and 'get_lngamma_dsp'
10
- # that changes dynamically if the values for 'free_volume', 'dispersion'
11
- # or "combinatorial" are changed after initialization of an instance.
12
- self._free_volume = free_volume
13
- self._dispersion = dispersion
19
+ # that changes dynamically if the values for 'combinatorial' or
20
+ # 'dispersion' are changed after initialization of an instance.
14
21
  self._combinatorial = combinatorial
22
+ self._dispersion = dispersion
15
23
 
16
24
  @ActModel.vectorize
17
25
  def lngamma(self, T, x):
@@ -54,8 +62,8 @@ class COSMOSAC(ActModel):
54
62
  (can replace ln_gamma_comb of normal COSMO-SAC) - Kuo2013
55
63
  x, v_298, v_hc are 1D arrays (number of elements = number of components)
56
64
  """
57
- v_298 = np.array([c.v_298 for c in self.mix])
58
- v_hc = np.array([c.v_hc for c in self.mix])
65
+ v_298 = np.array([comp.v_298 for comp in self.mixture])
66
+ v_hc = np.array([comp.v_hc for comp in self.mixture])
59
67
  vf = v_298-v_hc
60
68
  sum_vf = np.sum(x*vf)
61
69
  phix = vf/sum_vf
@@ -68,12 +76,12 @@ class COSMOSAC(ActModel):
68
76
  return self.COSMO.get_lngamma_resid(T, x)
69
77
 
70
78
  def get_lngamma_comb(self, x):
71
- if not self._combinatorial:
79
+ if self._combinatorial is False:
72
80
  return np.zeros(len(x))
73
- elif self._free_volume:
74
- return self.get_lngamma_fv(x)
75
- else:
81
+ elif self._combinatorial.lower() == 'sg':
76
82
  return self.get_lngamma_sg(x)
83
+ elif self._combinatorial.lower() == 'fv':
84
+ return self.get_lngamma_fv(x)
77
85
 
78
86
  def get_lngamma_disp(self, x):
79
87
  if self._dispersion:
@@ -81,14 +89,6 @@ class COSMOSAC(ActModel):
81
89
  else:
82
90
  return np.zeros(len(x))
83
91
 
84
- @property
85
- def free_volume(self):
86
- return self._free_volume
87
-
88
- @free_volume.setter
89
- def free_volume(self, value):
90
- self._free_volume = value
91
-
92
92
  @property
93
93
  def dispersion(self):
94
94
  return self._dispersion
@@ -102,5 +102,22 @@ class COSMOSAC(ActModel):
102
102
  return self._combinatorial
103
103
 
104
104
  @combinatorial.setter
105
- def combinatorial(self, value):
106
- self._combinatorial = value
105
+ def combinatorial(self, value: Union[str, bool]):
106
+ is_valid_string = isinstance(value, str) and value.lower() in ('sg', 'fv')
107
+ is_False = value is False
108
+ if is_valid_string or is_False:
109
+ self._combinatorial = value
110
+ else:
111
+ msg = "Invalid value for combinatorial term. Please choose 'sg', 'fv', or set to False."
112
+ raise ValueError(msg)
113
+
114
+ # =============================================================================
115
+ # Auxilliary functions
116
+ # =============================================================================
117
+ def configuration(self,
118
+ comb: Union[Literal['sg', 'fv'], bool] = 'sg',
119
+ dsp: bool = False, **kwargs
120
+ ):
121
+ """ Convenience function to quickly configure COSMO parameters """
122
+ self._combinatorial = comb
123
+ self._dispersion = dsp
@@ -0,0 +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 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)
@@ -1,35 +1,47 @@
1
- import pandas as pd
2
1
  import numpy as np
2
+ import pandas as pd
3
3
  from scipy.optimize import fsolve, root
4
- from typing import Literal
4
+ from numpy.typing import NDArray
5
+ from typing import Literal, Optional, Type, Union, List, Tuple, Generator, Dict
5
6
 
6
7
  from ..components import Component
8
+ from ..actmodels import ActModel
7
9
  from ..utils.spacing import spacing
8
10
 
11
+ NumericOrFrame = Union[float, List[float], Tuple[float, ...], NDArray[np.float64], pd.DataFrame]
12
+
9
13
  class SLE:
10
- def __init__(self, solute, solvent, actmodel):
11
- self.mix = [solute, solvent]
12
- self.model = actmodel
13
- self.solute, self.solvent = solute, solvent
14
+ def __init__(self,
15
+ actmodel: Union[ActModel, Type[ActModel]],
16
+ mixture: Optional[List[Component]] = None) -> None:
17
+ self.actmodel = actmodel
18
+ self.mixture = mixture
19
+ self._validate_arguments()
20
+ # Assign 'solute' and 'solvent' based on order in 'mixture'
21
+ # Default assignment can be changed in e.g. 'solubility()'
22
+ self.solute, self.solvent = self.mixture
14
23
 
15
24
  def solubility(self,
16
- solute: Component = None, solvent: Component = None,
17
- args=None, init=None, data=None,
25
+ solute: Optional[Component] = None,
26
+ solvent: Optional[Component] = None,
18
27
  vary: Literal['T', 'w', 'auto'] = 'auto',
19
- mix: Literal['ideal', 'real'] = 'real',
28
+ mix_type: Literal['ideal', 'real'] = 'real',
29
+ args: Optional[NumericOrFrame] = None,
30
+ init: Optional[NumericOrFrame] = None,
20
31
  solver: Literal['root', 'fsolve'] = 'root',
21
- show_progress=False):
32
+ show_progress=False, **kwargs):
22
33
  ''' Calculate solubility curve of solute in solvent.'''
23
34
  self.solute = solute or self.solute
24
35
  self.solvent = solvent or self.solvent
25
- self.vary, self.mix_type = vary, mix
36
+ self.vary, self.mix_type = vary, mix_type
26
37
  self.show_progress = show_progress
38
+ self.config = getattr(self.actmodel, 'config', self.mix_type)
27
39
  if self.vary == 'auto':
28
40
  gen = self.auto_solve(solver)
29
41
  else:
30
42
  self._vary = self.vary
31
- if args is None or init is None:
32
- args, init = self.initialize(init=init, data=data)
43
+ args = self.set_args(args)
44
+ init = self.set_x0(init)
33
45
  gen = self.solve_sle(args, init, solver)
34
46
  res = [k for k in gen]
35
47
  res = pd.DataFrame(res, columns=['T', 'x', 'vary', 'w'])
@@ -40,33 +52,40 @@ class SLE:
40
52
  # =============================================================================
41
53
  # MATHEMATICS
42
54
  # =============================================================================
43
- def solve_sle(self, args, init, solver='root'):
44
- is_iterable = hasattr(init, "__len__") and len(init) > 1
55
+ def solve_sle(self, args: NDArray[np.float64], init: NDArray[np.float64],
56
+ solver: Literal['root', 'fsolve'] = 'root'
57
+ ) -> Generator[Dict[str, Union[float, str]], None, None]:
58
+ # Check compatibility of the "init" values
59
+ is_iterable = init.size > 1
60
+ if is_iterable and not init.size == args.size:
61
+ msg = 'The length of "init" must be the same as "args".'
62
+ raise ValueError(msg)
63
+ x0 = init
64
+ # Setup solver and handle pure component case
45
65
  key, lock = ['T', 'x'] if self._vary == 'T' else ['x', 'T']
46
66
  solve = self.set_solver(solver=solver)
47
- x0 = init
48
67
  args, pure_component = self._handle_pure_component(args)
49
68
  if pure_component: # no need to calculate pure component
50
69
  yield pure_component
51
-
52
70
  for i, arg in enumerate(args):
53
71
  x0 = init[i] if is_iterable else x0
54
72
  out = float(solve(x0, arg))
55
73
  x0 = out if not is_iterable else x0
56
74
  res = {key: arg, lock: out, 'vary': self._vary}
57
- res['w'] = self.model._convert(res['x'])[0]
75
+ res['w'] = self.actmodel._convert(res['x'])[0]
58
76
  text = (f"T={res['T']:.2f}", f"w={res['w']:.4f}", f"x={res['x']:.4f}")
59
77
  if self.show_progress:
60
- print(f'SLE ({self.mix_type}): ', *text)
78
+ print(f'SLE ({self.config}): ', *text)
61
79
  yield res
62
80
 
63
81
  def auto_solve(self, solver: Literal['root', 'fsolve'] = 'root'):
64
82
  if self.show_progress:
65
83
  print()
66
- print(f"Calculating SLE ({self.mix_type})...")
84
+ print(f"Calculating SLE ({self.config})...")
67
85
  # Start with varying 'w' until dTdw > THRESHOLD
68
86
  self._vary = 'w'
69
- args, x0 = self.initialize()
87
+ args = self.set_args()
88
+ x0 = self.set_x0()
70
89
  gen = self.solve_sle(args, x0, solver)
71
90
  previous = None
72
91
  for i, current in enumerate(gen):
@@ -77,10 +96,7 @@ class SLE:
77
96
  # Switch to varying 'T'
78
97
  self._vary = 'T'
79
98
  T0, x0 = current['T'], current['x']
80
- # # (Deprecated): If last dT>5, make the next dT=5 (from old version)
81
- # T1 = previous['T']; dT = T0 - T1
82
- # T0 += dT if abs(dT) < 5 else np.sign(dT) * 5
83
- args = self.set_args(xmax=T0)[1:] # exclude initial point (redundant)
99
+ args = self.set_args(xmax=T0)[1:] # exclude initial point
84
100
  gen = self.solve_sle(args, x0)
85
101
  yield from gen
86
102
 
@@ -92,7 +108,7 @@ class SLE:
92
108
  return np.exp(-self.gibbs_fusion(T))
93
109
 
94
110
  def real_mix(self, T, x):
95
- lngamma = self.model.lngamma(T, x)[0]
111
+ lngamma = self.actmodel.lngamma(T, x)[0]
96
112
  return np.log(x) + lngamma + self.gibbs_fusion(T)
97
113
 
98
114
  # Gibbs energy of fusion, i.e., the right-hand side of the solubility equation:
@@ -115,12 +131,12 @@ class SLE:
115
131
  # =============================================================================
116
132
  # HELPER FUNCTIONS
117
133
  # =============================================================================
118
- def initialize(self, xmin=None, xmax=None, dx=None, data=None, init=None):
119
- args = self.set_args(xmin, xmax, dx, data)
120
- x0 = self.set_x0(init)
121
- return args, x0
122
-
123
- def set_args(self, xmin=None, xmax=None, dx=None, data=None):
134
+ def set_args(self,
135
+ args: Optional[NumericOrFrame] = None,
136
+ xmin: Optional[float] = None,
137
+ xmax: Optional[float] = None,
138
+ dx: Optional[float] = None
139
+ ) -> NDArray[np.float64]:
124
140
  vary = self._vary
125
141
  # Determine argument values based on input data or generate
126
142
  # them based on range and type
@@ -132,7 +148,7 @@ class SLE:
132
148
  ma = defaults[vary]['max'] if xmax is None else xmax
133
149
  dx = defaults[vary]['step'] if dx is None else dx
134
150
 
135
- if data is None:
151
+ if args is None:
136
152
  if self.vary != 'auto': # auto_vary == False
137
153
  args = np.arange(ma, mi-dx, -dx)
138
154
  args[-1] = np.maximum(args[-1], mi)
@@ -145,17 +161,18 @@ class SLE:
145
161
  else: # vary == 'w'
146
162
  num = 16 if self.mix_type == 'ideal' else 21
147
163
  args = spacing(ma, mi, num, 'quadratic')
148
- else:
149
- args = data
150
- return args if vary != 'w' else self.model._convert(args, to='mole')
164
+ args = np.asarray(args)
165
+ args = args if vary != 'w' else self.actmodel._convert(args, to='mole')
166
+ return args
151
167
 
152
- def set_x0(self, init=None):
168
+ def set_x0(self, init: Optional[NumericOrFrame] = None) -> NDArray[np.float64]:
153
169
  vary = self._vary
154
170
  # Set up initial values based on the type of variable ('T' or 'w')
155
171
  if vary == 'T':
156
- x0 = 1. if init is None else self.model._convert(init, to='mole')
172
+ x0 = 1. if init is None else self.actmodel._convert(init, to='mole')
157
173
  else: # vary == 'w'
158
174
  x0 = self.solute.T_fus if init is None else init
175
+ x0 = np.asarray(x0)
159
176
  return x0
160
177
 
161
178
  def set_solver(self, solver: Literal['root', 'fsolve'] = 'root'):
@@ -205,3 +222,19 @@ class SLE:
205
222
  args = args[args != 1]
206
223
  return args, res
207
224
  return args, None
225
+
226
+ def _validate_arguments(self):
227
+ """Validate the arguments for the SLE class."""
228
+ # TODO: Insert case where both actmodel and mixture are provided (check if acmodel.mixture == mixture, if not raise warning)
229
+ if isinstance(self.actmodel, ActModel):
230
+ # If actmodel is an instance of ActModel
231
+ self.mixture: List[Component] = self.mixture or self.actmodel.mixture
232
+ elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
233
+ # If actmodel is a class (subclass of ActModel)
234
+ if self.mixture is None:
235
+ raise ValueError("Please provide a valid mixture:Mixture.")
236
+ self.actmodel: ActModel = self.actmodel(self.mixture)
237
+ else:
238
+ # If actmodel is neither an instance nor a subclass of ActModel
239
+ err = "'actmodel' must be an instance or a subclass of 'ActModel'"
240
+ raise ValueError(err)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cosmopharm
3
- Version: 0.0.22
3
+ Version: 0.0.23
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
@@ -38,13 +38,13 @@ Requires-Dist: matplotlib>=3.0; extra == "examples"
38
38
 
39
39
  # COSMOPharm
40
40
 
41
+ Welcome to the COSMOPharm package, accompanying [our paper in *J. Chem. Theory Comput.*](https://dx.doi.org/10.1021/acs.jctc.9b01016). This project and its associated publication offer insights and a practical toolkit for researching drug-polymer and drug-solvent systems, aiming to provide the scientific community with the means to reproduce our findings and further the development of COSMO-SAC-based models.
42
+
41
43
  <p align="center">
42
44
  <!-- <img src="https://github.com/usnistgov/COSMOSAC/raw/master/JCTC2020.PNG" alt="TOC Figure" width="500"> -->
43
45
  <img src="https://github.com/usnistgov/COSMOSAC/raw/master/JCTC2020.PNG" alt="TOC Figure">
44
46
  </p>
45
47
 
46
- Welcome to the COSMOPharm package, accompanying [our paper in *J. Chem. Theory Comput.*](https://dx.doi.org/10.1021/acs.jctc.9b01016). This project and its associated publication offer insights and a practical toolkit for researching drug-polymer and drug-solvent systems, aiming to provide the scientific community with the means to reproduce our findings and further the development of COSMO-SAC-based models.
47
-
48
48
  ## About
49
49
 
50
50
  COSMOPharm is a Python package designed for predictive modeling of drug-polymer compatibility and drug-solubility in common solvents. It leverages the COSMO-SAC (Conductor-like Screening Model Segment Activity Coefficient) model, offering a robust platform for solubility, miscibility, and phase behavior prediction in drug formulation processes.
@@ -62,7 +62,7 @@ Install COSMOPharm with pip:
62
62
 
63
63
  `pip install cosmopharm`
64
64
 
65
- Ensure you have installed the cCOSMO library as per instructions on the [COSMOSAC GitHub page](https://github.com/usnistgov/COSMOSAC).
65
+ Ensure you have installed the `cCOSMO` library as per instructions on the [COSMOSAC GitHub page](https://github.com/usnistgov/COSMOSAC).
66
66
 
67
67
  ## Quick Start
68
68
 
@@ -1,83 +0,0 @@
1
- import numpy as np
2
- import numbers
3
-
4
- from ..utils.convert import convert
5
-
6
- class ActModel:
7
-
8
- def __init__(self, components: list = []):
9
- self.mix = components
10
-
11
- def lngamma(self, T, x):
12
- pass
13
-
14
- def dlngamma(self, T, x):
15
- # Only binary case
16
- def f(x1):
17
- x = np.array([x1, 1-x1])
18
- return self.lngamma(T, x)#[0]
19
- h, x = 0.0001, x[0]
20
- dy = (f(x+h)-f(x-h))/(2*h)
21
- # Revert direction of dy2_dx2 --> dy2_dx1
22
- dy[1] = dy[1][::-1]
23
- return f(x), dy
24
-
25
- def activity(self, T, x):
26
- act = np.log(x) + self.lngamma(T, x)
27
- act[(act == np.inf) | (act == -np.inf)] = np.nan
28
- return act
29
-
30
- def gmix(self, T, x):
31
- # Convert input as needed
32
- x = self._convert_input(x)
33
- # Create mask to identify columns that don't contain 0 or 1
34
- mask = np.any((x != 0) & (x != 1), axis=0)
35
- # Apply the mask to filter x
36
- _x = x[:, mask]
37
- # Calculate gmix for the x values
38
- _gmix = _x * (np.log(_x) + self.lngamma(T, _x))
39
- _gmix = np.sum(_gmix, axis=0)
40
- # Initialize gmix array with zeros
41
- gmix = np.zeros(x.shape[1])
42
- # Fill gmix with calculated values where the mask is True
43
- gmix[mask] = _gmix
44
- return gmix
45
-
46
-
47
- # =============================================================================
48
- # Wrapper functions (Decorators)
49
- # =============================================================================
50
- @staticmethod
51
- def vectorize(func):
52
- ''' Intended vor ActModels where only single mole fractions can be
53
- handled, like e.g. COSMO-SAC. This function vectorizes the lngamma()
54
- to make it work with arrays of mole fractions.
55
- '''
56
- def wrapper(self, T, x):
57
- # Convert input to appropriate format
58
- x = self._convert_input(x)
59
- # Process based on the dimensionality of x
60
- if x.ndim == 1:
61
- return func(self, T, x)
62
- elif x.ndim == 2:
63
- results = [func(self, T, x[:, col]) for col in range(x.shape[1])]
64
- return np.array(results).T
65
- else:
66
- raise ValueError("Input must be either a scalar, 0D, 1D or 2D array")
67
- return wrapper
68
-
69
-
70
- # =============================================================================
71
- # Auxilliary functions
72
- # =============================================================================
73
- def _convert_input(self, x):
74
- """Converts input to a 1-dim ndarray if it's a number or 0-dim ndarray."""
75
- if isinstance(x, numbers.Number) or (isinstance(x, np.ndarray) and x.ndim == 0):
76
- return np.array([float(x), 1 - float(x)])
77
- elif isinstance(x, np.ndarray) and x.ndim == 1 and len(x) != len(self.mix):
78
- return np.array([x, 1 - x])
79
- return x
80
-
81
- def _convert(self, x, to='weight'):
82
- Mw = np.array([c.Mw for c in self.mix])
83
- return convert(x=np.array([x, 1-x]), Mw=Mw, to=to)[0]
@@ -1,89 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
- from scipy.optimize import least_squares
4
-
5
- from ..utils import spacing
6
- from ..utils.lle_scanner import estimate_lle_from_gmix
7
-
8
-
9
- class LLE:
10
- def __init__(self, actmodel):
11
- self.mix = actmodel.mix
12
- self.model = actmodel
13
-
14
- def fobj_binodal(self, x1, T):
15
- # Equilibrium: Isoactivity criterion (aL1 - aL2 = 0)
16
- x = np.array([x1, 1-x1])
17
- activity = self.model.activity(T, x)
18
- equilibrium = np.diff(activity, axis=1)
19
- return equilibrium.ravel() # reshape from (2,1) --> (2,)
20
-
21
- def fobj_spinodal(self, x1):
22
- T = 0
23
- x = np.array([x1, 1-x1])
24
- return self.model.thermofac(T, x)
25
-
26
- def binodal(self, T, x0=None):
27
- if x0 is None:
28
- x0 = [0.1, 0.999] # 1_N2_Ethan
29
- kwargs = dict(bounds=(0,1), ftol=1e-15, xtol=1e-15)
30
- res = least_squares(self.fobj_binodal, x0, args=(T,), **kwargs)
31
- return res.x
32
-
33
- def spinodal(self, x0=None):
34
- if x0 is None:
35
- x0 = self.binodal()
36
- return least_squares(self.fobj_spinodal, x0).x
37
-
38
- # =============================================================================
39
- #
40
- # =============================================================================
41
- def approx_init_x0(self, T):
42
- x1 = spacing(0,1,51,'poly',n=3)
43
- gmix = self.model.gmix(T, x1)
44
- xL, xR, yL, yR = estimate_lle_from_gmix(x1, gmix, rough=True)
45
- return xL, xR
46
-
47
- def solve_lle(self, T, x0, info=True):
48
- binodal_x = self.binodal(T, x0)
49
- binodal_w = self.model._convert(binodal_x)
50
- formatted_w_binodal = [f"wL{i+1}={value:.4f}" for i, value in enumerate(binodal_w)]
51
- formatted_x_binodal = [f"xL{i+1}={value:.6f}" for i, value in enumerate(binodal_x)]
52
- msg = ('LLE: ', f"{T=:.2f}", *formatted_w_binodal, *formatted_x_binodal)
53
- if info:
54
- print(*msg)
55
- return binodal_x, binodal_w
56
- return binodal_x, binodal_w, msg
57
-
58
- def miscibility(self, T, x0=None, max_gap=0.1, max_T=500, dT=25):
59
- print()
60
- print("Calculating LLE...")
61
- res = []
62
-
63
- if x0 is None:
64
- print("...searching for suitable initial value...")
65
- x0 = self.approx_init_x0(T)
66
- binodal_x, binodal_w, msg = self.solve_lle(T, x0, info=False)
67
-
68
- # Check if initial guess is reasonalble - otherwise increase T
69
- while binodal_x[0] < x0[0] and T <= max_T:
70
- print('LLE: ', f"{T=:.2f}", "...no feasbible initial value found.")
71
- T += 10 # Increase T by 10
72
- x0 = self.approx_init_x0(T)
73
- binodal_x, binodal_w, msg = self.solve_lle(T, x0, info=False)
74
- print("Suitable initial value found! Proceed with calculating LLE...")
75
- print(*msg)
76
- gap = np.diff(binodal_w)[0]
77
- res.append((T, *binodal_w, *binodal_x))
78
-
79
- exponent = 2.1
80
- while gap > max_gap and T <= max_T:
81
- T += dT * gap**exponent
82
- x0 = binodal_x
83
- binodal_x, binodal_w = self.solve_lle(T, x0)
84
- gap = np.diff(binodal_w)[0]
85
- res.append((T, *binodal_w, *binodal_x))
86
-
87
- columns = ['T', 'wL1', 'wL2', 'xL1', 'xL2']
88
- res = pd.DataFrame(res, columns=columns)
89
- return res
File without changes
File without changes