cosmopharm 0.0.22__tar.gz → 0.0.23.1__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 (26) hide show
  1. {cosmopharm-0.0.22/src/cosmopharm.egg-info → cosmopharm-0.0.23.1}/PKG-INFO +4 -4
  2. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/README.md +48 -16
  3. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/setup.cfg +2 -3
  4. cosmopharm-0.0.23.1/src/cosmopharm/actmodels/actmodel.py +107 -0
  5. cosmopharm-0.0.23.1/src/cosmopharm/actmodels/cosmo.py +154 -0
  6. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/components.py +7 -6
  7. cosmopharm-0.0.23.1/src/cosmopharm/equilibrium/lle.py +126 -0
  8. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/equilibrium/sle.py +79 -42
  9. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/helpers.py +2 -2
  10. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1/src/cosmopharm.egg-info}/PKG-INFO +4 -4
  11. cosmopharm-0.0.22/src/cosmopharm/actmodels/actmodel.py +0 -83
  12. cosmopharm-0.0.22/src/cosmopharm/actmodels/cosmo.py +0 -106
  13. cosmopharm-0.0.22/src/cosmopharm/equilibrium/lle.py +0 -89
  14. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/LICENSE +0 -0
  15. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/setup.py +0 -0
  16. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/__init__.py +0 -0
  17. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/actmodels/__init__.py +0 -0
  18. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/equilibrium/__init__.py +0 -0
  19. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/__init__.py +0 -0
  20. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/convert.py +0 -0
  21. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/lle_scanner.py +0 -0
  22. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/spacing.py +0 -0
  23. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/SOURCES.txt +0 -0
  24. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/dependency_links.txt +0 -0
  25. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/requires.txt +0 -0
  26. {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/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.1
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
 
@@ -5,7 +5,7 @@
5
5
  </p>
6
6
 
7
7
 
8
- Welcome to the COSMOPharm repository, 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.
8
+ Welcome to the COSMOPharm repository, accompanying [our paper in *Molecular Pharmaceutics*](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.
9
9
 
10
10
  ## About
11
11
 
@@ -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
@@ -35,9 +35,9 @@ For users who need more control over the installation process (e.g., for develop
35
35
 
36
36
  #### Step 1: Clone the Repository
37
37
 
38
- First, clone the COSMOPharm repository:
38
+ First, clone the COSMOPharm repository. The project includes a submodule named "pharmaceuticals" that stores essential data files (.sigma, .cosmo, and .xyz files) for the pharmaceutical components. Use the --recurse-submodules option to ensure that the "pharmaceuticals" submodule is correctly initialized and updated along with the main project.
39
39
  ```
40
- git clone https://github.com/ivanantolo/cosmopharm
40
+ git clone --recurse-submodules https://github.com/ivanantolo/cosmopharm
41
41
  ```
42
42
 
43
43
  #### Step 2: Navigate to the Repository Directory
@@ -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
 
@@ -73,22 +73,23 @@ Get started with COSMOPharm using the minimal example below, which demonstrates
73
73
 
74
74
 
75
75
  ```python
76
+ import matplotlib.pyplot as plt
76
77
  import cCOSMO
77
78
  from cosmopharm import SLE, COSMOSAC
78
79
  from cosmopharm.utils import create_components, read_params
79
80
 
80
- # Define components - replace 'DrugName' and 'PolymerName' with your actual component names
81
- names = ['DrugName', 'PolymerName']
82
- params_file = "path/to/your/params.xlsx"
81
+ # Define components
82
+ names = ['SIM','PLGA50']
83
+ params_file = "data/sle/table_params.xlsx"
83
84
 
84
85
  # Load parameters and create components
85
86
  parameters = read_params(params_file)
86
- components = create_components(names, parameters)
87
+ mixture = create_components(names, parameters)
87
88
 
88
89
  # Initialize COSMO-SAC model - replace paths with your local paths to COSMO profiles
89
90
  db = cCOSMO.DelawareProfileDatabase(
90
- "path/to/your/complist/complist.txt",
91
- "path/to/your/profiles/")
91
+ "./profiles/_import_methods/UD/complist.txt",
92
+ "./profiles/_import_methods/UD/sigma3/")
92
93
 
93
94
  for name in names:
94
95
  iden = db.normalize_identifier(name)
@@ -96,16 +97,32 @@ for name in names:
96
97
  COSMO = cCOSMO.COSMO3(names, db)
97
98
 
98
99
  # Setup the COSMO-SAC model with components
99
- model = COSMOSAC(COSMO, components=components)
100
+ actmodel = COSMOSAC(COSMO, mixture=mixture)
100
101
 
101
102
  # Calculate solubility (SLE)
102
- sle = SLE(solute=components[0], solvent=components[1], actmodel=model)
103
+ sle = SLE(actmodel=actmodel)
103
104
  solubility = sle.solubility(mix='real')
104
105
 
105
106
  # Output the solubility
106
107
  print(solubility[['T', 'w', 'x']].to_string(index=False))
108
+
109
+ # Plot results
110
+ plt.plot(*solubility[['w','T']].values.T,'.-', label='Solubility (w)')
111
+
112
+ # Settings
113
+ plt.xlim(0,1)
114
+ plt.ylim(300,500)
115
+ # Adding title and labels
116
+ plt.title('Solubility vs. Temperature')
117
+ plt.ylabel("T / K")
118
+ xlabel = {'w':'Weight', 'x':'Mole'}
119
+ plt.xlabel(f"Weight fraction {mixture[0].name}")
120
+ plt.legend()
121
+ # Save the figure to a PNG or PDF file
122
+ plt.savefig('solubility_plot.png') # Saves the plot as a PNG file
123
+ # plt.savefig('solubility_plot.pdf') # Saves the plot as a PDF file
124
+ plt.show()
107
125
  ```
108
- Replace 'DrugName', 'PolymerName', and file paths with your actual data and files.
109
126
 
110
127
  For a more comprehensive demonstration, including advanced functionalities and plotting results, please see the [example_usage.py](https://github.com/ivanantolo/cosmopharm/blob/main/example_usage.py) script in this repository. This detailed example walks through the process of setting up COSMOPharm, initializing models, and visualizing the results of solubility and miscibility calculations.
111
128
 
@@ -118,8 +135,23 @@ For bugs, feature requests, or other queries, please [open an issue](https://git
118
135
 
119
136
  ## Citation
120
137
 
121
- If you use COSMOPharm in your research, please consider citing it. You can find the citation format in [CITATION.md](https://github.com/ivanantolo/cosmopharm/CITATION.md).
122
-
138
+ We appreciate citations to our work as they help acknowledge and spread our research contributions. If you use COSMOPharm in your research, please cite the associated paper. Citation details are provided in the [`CITATION.cff`](https://github.com/ivanantolo/cosmopharm/CITATION.cff) file, and GitHub generates APA or BibTeX formats accessible under the "Cite this repository" dropdown on our repository page.
139
+
140
+ For convenience, here's the citation in BibTeX format:
141
+
142
+ ```bibtex
143
+ @article{Antolovic2024COSMOPharm,
144
+ title={COSMOPharm: Drug--Polymer Compatibility of Pharmaceutical Amorphous Solid Dispersions from COSMO-SAC},
145
+ author={Antolovic, Ivan and Vrabec, Jadran and Klajmon, Martin},
146
+ journal={Molecular Pharmaceutics},
147
+ year={2024},
148
+ volume={1}, # Will be adjusted accordingly
149
+ issue={1}, # Will be adjusted accordingly
150
+ month={3}, # Will be adjusted accordingly
151
+ pages={1--10}, # Will be adjusted accordingly
152
+ doi={10.1021/acs.molpharmaceut.3c12345} # Will be adjusted accordingly
153
+ }
154
+ ```
123
155
 
124
156
  ## License
125
157
 
@@ -1,12 +1,11 @@
1
1
  [metadata]
2
2
  name = cosmopharm
3
- version = 0.0.22
3
+ version = 0.0.23.1
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]
@@ -0,0 +1,154 @@
1
+
2
+ import numpy as np
3
+ import cCOSMO
4
+
5
+ from typing import List, Union, Literal
6
+ from .actmodel import ActModel
7
+ from ..components import Component
8
+
9
+
10
+ class COSMOSAC(ActModel):
11
+ # Handling invalid values for free volume calculation
12
+ class InvalidFreeVolumeParametersException(Exception):
13
+ pass
14
+
15
+ def __init__(self,
16
+ COSMO: Union[cCOSMO.COSMO1, cCOSMO.COSMO3],
17
+ mixture: List[Component],
18
+ combinatorial: Union[Literal['sg', 'fv'], bool] = 'sg',
19
+ dispersion: bool = False,
20
+ ) -> None:
21
+ self.COSMO = COSMO
22
+ self.mixture = mixture
23
+ # Flexible assignment of 'get_lngamma_comb' and 'get_lngamma_dsp'
24
+ # that changes dynamically if the values for 'combinatorial' or
25
+ # 'dispersion' are changed after initialization of an instance.
26
+ self._combinatorial = combinatorial
27
+ self._dispersion = dispersion
28
+
29
+ @ActModel.vectorize
30
+ def lngamma(self, T, x):
31
+ resid = self.get_lngamma_resid(T, x)
32
+ comb = self.get_lngamma_comb(x)
33
+ disp = self.get_lngamma_disp(x)
34
+ lngamma = resid + comb + disp
35
+ return lngamma
36
+
37
+ def get_lngamma_fv(self, x):
38
+ """
39
+ Calculates the free-volume term of the activity coefficient for a mixture.
40
+
41
+ This implementation uses a formula to avoid numerical instability when
42
+ `x_i` approaches zero, which is important in asymmetric API-polymer
43
+ mixtures. The formula used is:
44
+
45
+ ```
46
+ phi_i^FV / x_i = v_i^F / sum_j(x_j * v_j^F)
47
+ ```
48
+
49
+ where
50
+ - `phi_i^FV` is the free-volume fraction of component `i`,
51
+ - `x_i` is the mole fraction of component `i`,
52
+ - `v_i^F` is the free volume of component `i`,
53
+ and the summation is over all components `j` in the mixture.
54
+
55
+ Parameters
56
+ ----------
57
+ x : array_like
58
+ Mole fractions of the components in the mixture.
59
+
60
+ Returns
61
+ -------
62
+ np.ndarray
63
+ Logarithm of the free-volume term of the activity coefficient.
64
+
65
+ Note:
66
+ Free-volume term of the activity coefficient according to Elbro et al.
67
+ (can replace ln_gamma_comb of normal COSMO-SAC) - Kuo2013
68
+ x, v_298, v_hc are 1D arrays (number of elements = number of components)
69
+ """
70
+ # TODO: Make sure, that v_298 and v_hc are provided, else "FV" not possible
71
+ self.validate_free_volume_parameters() # Ensure components are valid before proceeding
72
+ v_298 = np.array([comp.v_298 for comp in self.mixture])
73
+ v_hc = np.array([comp.v_hc for comp in self.mixture])
74
+ vf = v_298-v_hc
75
+ sum_vf = np.sum(x*vf)
76
+ phix = vf/sum_vf
77
+ return np.log(phix) + 1 - phix
78
+
79
+ def get_lngamma_sg(self, x):
80
+ return self.COSMO.get_lngamma_comb(0, x)
81
+
82
+ def get_lngamma_resid(self, T, x):
83
+ return self.COSMO.get_lngamma_resid(T, x)
84
+
85
+ def get_lngamma_comb(self, x):
86
+ if self._combinatorial is False:
87
+ return np.zeros(len(x))
88
+ elif self._combinatorial.lower() == 'sg':
89
+ return self.get_lngamma_sg(x)
90
+ elif self._combinatorial.lower() == 'fv':
91
+ return self.get_lngamma_fv(x)
92
+
93
+ def get_lngamma_disp(self, x):
94
+ if self._dispersion:
95
+ return self.COSMO.get_lngamma_disp(x)
96
+ else:
97
+ return np.zeros(len(x))
98
+
99
+ @property
100
+ def dispersion(self):
101
+ return self._dispersion
102
+
103
+ @dispersion.setter
104
+ def dispersion(self, value):
105
+ self._dispersion = value
106
+
107
+ @property
108
+ def combinatorial(self):
109
+ return self._combinatorial
110
+
111
+ @combinatorial.setter
112
+ def combinatorial(self, value: Union[str, bool]):
113
+ is_valid_string = isinstance(value, str) and value.lower() in ('sg', 'fv')
114
+ is_False = value is False
115
+ if is_valid_string or is_False:
116
+ self._combinatorial = value
117
+ else:
118
+ msg = "Invalid value for combinatorial term. Please choose 'sg', 'fv', or set to False."
119
+ raise ValueError(msg)
120
+
121
+ # =============================================================================
122
+ # Auxilliary functions
123
+ # =============================================================================
124
+ def configuration(self,
125
+ comb: Union[Literal['sg', 'fv'], bool] = 'sg',
126
+ dsp: bool = False, **kwargs
127
+ ):
128
+ """ Convenience function to quickly configure COSMO parameters """
129
+ self._combinatorial = comb
130
+ self._dispersion = dsp
131
+
132
+
133
+ def validate_free_volume_parameters(self):
134
+ # List of parameters to validate
135
+ parameters_to_check = ["v_298", "v_hc"]
136
+
137
+ for comp in self.mixture:
138
+ invalid_params = [] # List to accumulate names of invalid parameters for this component
139
+ for param in parameters_to_check:
140
+ value = getattr(comp, param, None)
141
+ # Check if value is None, not a number (np.nan), less than or equal to 0
142
+ if value is None or np.isnan(value) or value <= 0:
143
+ invalid_params.append((param, value)) # Append parameter name and value tuple
144
+
145
+ # Check if any errors were found for this component
146
+ if invalid_params:
147
+ # If errors were found, construct the warning message
148
+ error_message = f"Invalid FV parameters for component {comp}: {invalid_params}"
149
+ raise self.InvalidFreeVolumeParametersException(error_message)
150
+
151
+ # Additionally check if v_298 and v_hc are equal
152
+ if comp.v_298 == comp.v_hc:
153
+ msg = f"v_298 and v_hc are equal for component {comp}: v_298={comp.v_298}, v_hc={comp.v_hc}"
154
+ raise self.InvalidFreeVolumeParametersException(msg)
@@ -2,12 +2,13 @@ from typing import Optional
2
2
  from numbers import Number
3
3
 
4
4
  class Component:
5
- def __init__(self, name: Optional[str] = None,
6
- Mw: Optional[Number] = None,
7
- T_fus: Optional[Number] = None,
8
- H_fus: Optional[Number] = None,
9
- Cp_fus_a_fit: Optional[Number] = None,
10
- Cp_fus_bT_fit: Optional[Number] = None,
5
+ def __init__(self,
6
+ name: Optional[str] = None,
7
+ Mw: Optional[Number] = None, # Positive number expected
8
+ T_fus: Optional[Number] = None, # Positive number expected
9
+ H_fus: Number = 0,
10
+ Cp_fus_a_fit: Number = 0,
11
+ Cp_fus_bT_fit: Number = 0,
11
12
  v_298: Optional[Number] = None,
12
13
  v_hc: Optional[Number] = None,
13
14
  ):
@@ -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,72 +1,95 @@
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
- res = [k for k in gen]
35
- res = pd.DataFrame(res, columns=['T', 'x', 'vary', 'w'])
36
- res = res[['T', 'w', 'x', 'vary']]
37
- return res
46
+ try:
47
+ res = [k for k in gen]
48
+ res = pd.DataFrame(res, columns=['T', 'x', 'vary', 'w'])
49
+ res = res[['T', 'w', 'x', 'vary']]
50
+ return res
51
+ except self.actmodel.InvalidFreeVolumeParametersException as e:
52
+ print(f"Warning: {e}") # Inform the user
53
+ return pd.DataFrame(columns=['T', 'w', 'x', 'vary'])
38
54
 
39
55
 
40
56
  # =============================================================================
41
57
  # MATHEMATICS
42
58
  # =============================================================================
43
- def solve_sle(self, args, init, solver='root'):
44
- is_iterable = hasattr(init, "__len__") and len(init) > 1
59
+ def solve_sle(self, args: NDArray[np.float64], init: NDArray[np.float64],
60
+ solver: Literal['root', 'fsolve'] = 'root'
61
+ ) -> Generator[Dict[str, Union[float, str]], None, None]:
62
+ # Check compatibility of the "init" values
63
+ is_iterable = init.size > 1
64
+ if is_iterable and not init.size == args.size:
65
+ msg = 'The length of "init" must be the same as "args".'
66
+ raise ValueError(msg)
67
+ x0 = init
68
+ # Setup solver and handle pure component case
45
69
  key, lock = ['T', 'x'] if self._vary == 'T' else ['x', 'T']
46
70
  solve = self.set_solver(solver=solver)
47
- x0 = init
48
71
  args, pure_component = self._handle_pure_component(args)
49
72
  if pure_component: # no need to calculate pure component
50
73
  yield pure_component
51
-
52
74
  for i, arg in enumerate(args):
53
75
  x0 = init[i] if is_iterable else x0
54
76
  out = float(solve(x0, arg))
55
77
  x0 = out if not is_iterable else x0
56
78
  res = {key: arg, lock: out, 'vary': self._vary}
57
- res['w'] = self.model._convert(res['x'])[0]
79
+ res['w'] = self.actmodel._convert(res['x'])[0]
58
80
  text = (f"T={res['T']:.2f}", f"w={res['w']:.4f}", f"x={res['x']:.4f}")
59
81
  if self.show_progress:
60
- print(f'SLE ({self.mix_type}): ', *text)
82
+ print(f'SLE ({self.config}): ', *text)
61
83
  yield res
62
84
 
63
85
  def auto_solve(self, solver: Literal['root', 'fsolve'] = 'root'):
64
86
  if self.show_progress:
65
87
  print()
66
- print(f"Calculating SLE ({self.mix_type})...")
88
+ print(f"Calculating SLE ({self.config})...")
67
89
  # Start with varying 'w' until dTdw > THRESHOLD
68
90
  self._vary = 'w'
69
- args, x0 = self.initialize()
91
+ args = self.set_args()
92
+ x0 = self.set_x0()
70
93
  gen = self.solve_sle(args, x0, solver)
71
94
  previous = None
72
95
  for i, current in enumerate(gen):
@@ -77,10 +100,7 @@ class SLE:
77
100
  # Switch to varying 'T'
78
101
  self._vary = 'T'
79
102
  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)
103
+ args = self.set_args(xmax=T0)[1:] # exclude initial point
84
104
  gen = self.solve_sle(args, x0)
85
105
  yield from gen
86
106
 
@@ -92,7 +112,7 @@ class SLE:
92
112
  return np.exp(-self.gibbs_fusion(T))
93
113
 
94
114
  def real_mix(self, T, x):
95
- lngamma = self.model.lngamma(T, x)[0]
115
+ lngamma = self.actmodel.lngamma(T, x)[0]
96
116
  return np.log(x) + lngamma + self.gibbs_fusion(T)
97
117
 
98
118
  # Gibbs energy of fusion, i.e., the right-hand side of the solubility equation:
@@ -115,12 +135,12 @@ class SLE:
115
135
  # =============================================================================
116
136
  # HELPER FUNCTIONS
117
137
  # =============================================================================
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):
138
+ def set_args(self,
139
+ args: Optional[NumericOrFrame] = None,
140
+ xmin: Optional[float] = None,
141
+ xmax: Optional[float] = None,
142
+ dx: Optional[float] = None
143
+ ) -> NDArray[np.float64]:
124
144
  vary = self._vary
125
145
  # Determine argument values based on input data or generate
126
146
  # them based on range and type
@@ -132,7 +152,7 @@ class SLE:
132
152
  ma = defaults[vary]['max'] if xmax is None else xmax
133
153
  dx = defaults[vary]['step'] if dx is None else dx
134
154
 
135
- if data is None:
155
+ if args is None:
136
156
  if self.vary != 'auto': # auto_vary == False
137
157
  args = np.arange(ma, mi-dx, -dx)
138
158
  args[-1] = np.maximum(args[-1], mi)
@@ -145,17 +165,18 @@ class SLE:
145
165
  else: # vary == 'w'
146
166
  num = 16 if self.mix_type == 'ideal' else 21
147
167
  args = spacing(ma, mi, num, 'quadratic')
148
- else:
149
- args = data
150
- return args if vary != 'w' else self.model._convert(args, to='mole')
168
+ args = np.asarray(args)
169
+ args = args if vary != 'w' else self.actmodel._convert(args, to='mole')
170
+ return args
151
171
 
152
- def set_x0(self, init=None):
172
+ def set_x0(self, init: Optional[NumericOrFrame] = None) -> NDArray[np.float64]:
153
173
  vary = self._vary
154
174
  # Set up initial values based on the type of variable ('T' or 'w')
155
175
  if vary == 'T':
156
- x0 = 1. if init is None else self.model._convert(init, to='mole')
176
+ x0 = 1. if init is None else self.actmodel._convert(init, to='mole')
157
177
  else: # vary == 'w'
158
178
  x0 = self.solute.T_fus if init is None else init
179
+ x0 = np.asarray(x0)
159
180
  return x0
160
181
 
161
182
  def set_solver(self, solver: Literal['root', 'fsolve'] = 'root'):
@@ -205,3 +226,19 @@ class SLE:
205
226
  args = args[args != 1]
206
227
  return args, res
207
228
  return args, None
229
+
230
+ def _validate_arguments(self):
231
+ """Validate the arguments for the SLE class."""
232
+ # TODO: Insert case where both actmodel and mixture are provided (check if acmodel.mixture == mixture, if not raise warning)
233
+ if isinstance(self.actmodel, ActModel):
234
+ # If actmodel is an instance of ActModel
235
+ self.mixture: List[Component] = self.mixture or self.actmodel.mixture
236
+ elif isinstance(self.actmodel, type) and issubclass(self.actmodel, ActModel):
237
+ # If actmodel is a class (subclass of ActModel)
238
+ if self.mixture is None:
239
+ raise ValueError("Please provide a valid mixture:Mixture.")
240
+ self.actmodel: ActModel = self.actmodel(self.mixture)
241
+ else:
242
+ # If actmodel is neither an instance nor a subclass of ActModel
243
+ err = "'actmodel' must be an instance or a subclass of 'ActModel'"
244
+ raise ValueError(err)
@@ -21,8 +21,8 @@ def add_parameters(c, params):
21
21
  c.Mw = params['Mw'] # g/mol
22
22
  c.T_fus = params['T_fus'] if params['T_fus'] > 0 else np.nan # K
23
23
  c.H_fus = params['H_fus'] * KILOJOULE_TO_JOULE # J/mol
24
- c.Cp_fus_A = params['Cp_fus_a_fit'] # J/(mol K)
25
- c.Cp_fus_BT = params['Cp_fus_bT_fit'] # J/(mol K²)
24
+ c.Cp_fus_A = np.nan_to_num(params['Cp_fus_a_fit']) # J/(mol K)
25
+ c.Cp_fus_BT = np.nan_to_num(params['Cp_fus_bT_fit']) # J/(mol K²)
26
26
  c.v_298 = params['v298'] # cm³/mol
27
27
  c.v_hc = params['v_hc'] # cm³/mol
28
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cosmopharm
3
- Version: 0.0.22
3
+ Version: 0.0.23.1
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,106 +0,0 @@
1
- import numpy as np
2
- from .actmodel import ActModel
3
-
4
- class COSMOSAC(ActModel):
5
- def __init__(self, COSMO, components: list, free_volume=False,
6
- dispersion=False, combinatorial=True):
7
- self.COSMO = COSMO
8
- self.mix = components
9
- # 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
14
- self._combinatorial = combinatorial
15
-
16
- @ActModel.vectorize
17
- def lngamma(self, T, x):
18
- resid = self.get_lngamma_resid(T, x)
19
- comb = self.get_lngamma_comb(x)
20
- disp = self.get_lngamma_disp(x)
21
- lngamma = resid + comb + disp
22
- return lngamma
23
-
24
- def get_lngamma_fv(self, x):
25
- """
26
- Calculates the free-volume term of the activity coefficient for a mixture.
27
-
28
- This implementation uses a formula to avoid numerical instability when
29
- `x_i` approaches zero, which is important in asymmetric API-polymer
30
- mixtures. The formula used is:
31
-
32
- ```
33
- phi_i^FV / x_i = v_i^F / sum_j(x_j * v_j^F)
34
- ```
35
-
36
- where
37
- - `phi_i^FV` is the free-volume fraction of component `i`,
38
- - `x_i` is the mole fraction of component `i`,
39
- - `v_i^F` is the free volume of component `i`,
40
- and the summation is over all components `j` in the mixture.
41
-
42
- Parameters
43
- ----------
44
- x : array_like
45
- Mole fractions of the components in the mixture.
46
-
47
- Returns
48
- -------
49
- np.ndarray
50
- Logarithm of the free-volume term of the activity coefficient.
51
-
52
- Note:
53
- Free-volume term of the activity coefficient according to Elbro et al.
54
- (can replace ln_gamma_comb of normal COSMO-SAC) - Kuo2013
55
- x, v_298, v_hc are 1D arrays (number of elements = number of components)
56
- """
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])
59
- vf = v_298-v_hc
60
- sum_vf = np.sum(x*vf)
61
- phix = vf/sum_vf
62
- return np.log(phix) + 1 - phix
63
-
64
- def get_lngamma_sg(self, x):
65
- return self.COSMO.get_lngamma_comb(0, x)
66
-
67
- def get_lngamma_resid(self, T, x):
68
- return self.COSMO.get_lngamma_resid(T, x)
69
-
70
- def get_lngamma_comb(self, x):
71
- if not self._combinatorial:
72
- return np.zeros(len(x))
73
- elif self._free_volume:
74
- return self.get_lngamma_fv(x)
75
- else:
76
- return self.get_lngamma_sg(x)
77
-
78
- def get_lngamma_disp(self, x):
79
- if self._dispersion:
80
- return self.COSMO.get_lngamma_disp(x)
81
- else:
82
- return np.zeros(len(x))
83
-
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
- @property
93
- def dispersion(self):
94
- return self._dispersion
95
-
96
- @dispersion.setter
97
- def dispersion(self, value):
98
- self._dispersion = value
99
-
100
- @property
101
- def combinatorial(self):
102
- return self._combinatorial
103
-
104
- @combinatorial.setter
105
- def combinatorial(self, value):
106
- self._combinatorial = value
@@ -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