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.
- {cosmopharm-0.0.22/src/cosmopharm.egg-info → cosmopharm-0.0.23.1}/PKG-INFO +4 -4
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/README.md +48 -16
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/setup.cfg +2 -3
- cosmopharm-0.0.23.1/src/cosmopharm/actmodels/actmodel.py +107 -0
- cosmopharm-0.0.23.1/src/cosmopharm/actmodels/cosmo.py +154 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/components.py +7 -6
- cosmopharm-0.0.23.1/src/cosmopharm/equilibrium/lle.py +126 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/equilibrium/sle.py +79 -42
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/helpers.py +2 -2
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1/src/cosmopharm.egg-info}/PKG-INFO +4 -4
- cosmopharm-0.0.22/src/cosmopharm/actmodels/actmodel.py +0 -83
- cosmopharm-0.0.22/src/cosmopharm/actmodels/cosmo.py +0 -106
- cosmopharm-0.0.22/src/cosmopharm/equilibrium/lle.py +0 -89
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/LICENSE +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/setup.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/__init__.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/actmodels/__init__.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/equilibrium/__init__.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/__init__.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/convert.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/lle_scanner.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm/utils/spacing.py +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/SOURCES.txt +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/dependency_links.txt +0 -0
- {cosmopharm-0.0.22 → cosmopharm-0.0.23.1}/src/cosmopharm.egg-info/requires.txt +0 -0
- {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.
|
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 *
|
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
|
81
|
-
names = ['
|
82
|
-
params_file = "
|
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
|
-
|
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
|
-
"
|
91
|
-
"
|
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
|
-
|
100
|
+
actmodel = COSMOSAC(COSMO, mixture=mixture)
|
100
101
|
|
101
102
|
# Calculate solubility (SLE)
|
102
|
-
sle = SLE(
|
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
|
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.
|
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,
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
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,
|
11
|
-
|
12
|
-
|
13
|
-
self.
|
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,
|
17
|
-
|
25
|
+
solute: Optional[Component] = None,
|
26
|
+
solvent: Optional[Component] = None,
|
18
27
|
vary: Literal['T', 'w', 'auto'] = 'auto',
|
19
|
-
|
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,
|
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
|
-
|
32
|
-
|
43
|
+
args = self.set_args(args)
|
44
|
+
init = self.set_x0(init)
|
33
45
|
gen = self.solve_sle(args, init, solver)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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,
|
44
|
-
|
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.
|
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.
|
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.
|
88
|
+
print(f"Calculating SLE ({self.config})...")
|
67
89
|
# Start with varying 'w' until dTdw > THRESHOLD
|
68
90
|
self._vary = 'w'
|
69
|
-
args
|
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
|
-
|
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.
|
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
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
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
|
-
|
149
|
-
|
150
|
-
return args
|
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.
|
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.
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|