flory 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flory/__init__.py +18 -0
- flory/_version.py +16 -0
- flory/detail/mcmp_impl.py +524 -0
- flory/mcmp.py +641 -0
- flory-0.0.0.dist-info/LICENSE +21 -0
- flory-0.0.0.dist-info/METADATA +82 -0
- flory-0.0.0.dist-info/RECORD +9 -0
- flory-0.0.0.dist-info/WHEEL +5 -0
- flory-0.0.0.dist-info/top_level.txt +1 -0
flory/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package :mod:`flory` provides methods and classes for phase separation in multicomponent mixtures based on Flory-Huggins theory.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# try reading version of the automatically generated module
|
|
7
|
+
from ._version import __version__
|
|
8
|
+
except ImportError:
|
|
9
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("flory")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
# package is not installed
|
|
15
|
+
__version__ = "unknown"
|
|
16
|
+
del PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
from .mcmp import CoexistingPhasesFinder, find_coexisting_phases
|
flory/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.0.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 0, 0)
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
""" The implementation details of the module :mod:`~flory.mcmp`.
|
|
2
|
+
|
|
3
|
+
:mod:`flory.detail.mcmp_impl` contains the implementation details of the module
|
|
4
|
+
:mod:`~flory.mcmp`. The main components of the module is the function
|
|
5
|
+
:meth:`multicomponent_self_consistent_metastep`, which implements the self consistent
|
|
6
|
+
iterations for minimizing the extended free energy functional, and the function
|
|
7
|
+
:meth:`get_clusters`, which finds the unique phases.
|
|
8
|
+
|
|
9
|
+
In this module, arguments of functions are always marked by `Const`, `Output` or
|
|
10
|
+
`Mutable`, to indicate whether the arguments will keep invariant, be directly overwritten,
|
|
11
|
+
or reused.
|
|
12
|
+
|
|
13
|
+
.. codeauthor:: Yicheng Qiang <yicheng.qiang@ds.mpg.de>
|
|
14
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numba as nb
|
|
18
|
+
import numpy as np
|
|
19
|
+
from scipy import cluster, spatial
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@nb.njit()
|
|
23
|
+
def count_valid_compartments(Js: np.ndarray, threshold: float) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Count how many entries in :paramref:`~count_valid_compartments.Js` are larger than
|
|
26
|
+
:paramref:`~count_valid_compartments.threshold`.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
Js:
|
|
30
|
+
Const. 1D array with the size of :math:`M`.
|
|
31
|
+
threshold:
|
|
32
|
+
Const. The threshold value.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
: Number of entries in :paramref:`Js` larger than :paramref:`threshold`.
|
|
36
|
+
"""
|
|
37
|
+
return (Js > threshold).sum()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@nb.njit()
|
|
41
|
+
def make_valid_compartment_masks(Js: np.ndarray, threshold: float) -> np.ndarray:
|
|
42
|
+
"""
|
|
43
|
+
Create masks for entries in :paramref:`~make_valid_compartment_masks.Js` are larger
|
|
44
|
+
than :paramref:`~make_valid_compartment_masks.threshold`. Value of 1.0 or 0.0
|
|
45
|
+
indicates a valid or invalid mask, respectively.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
Js:
|
|
49
|
+
Const. 1D array with the size of :math:`M`.
|
|
50
|
+
threshold:
|
|
51
|
+
Const. The threshold value.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
:
|
|
55
|
+
1D array with the size of :math:`M`, containing masks of entries in
|
|
56
|
+
:paramref:`Js` larger than :paramref:`threshold`.
|
|
57
|
+
"""
|
|
58
|
+
return np.sign(Js - threshold).clip(0.0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@nb.njit()
|
|
62
|
+
def revive_compartments_by_random(
|
|
63
|
+
Js: np.ndarray,
|
|
64
|
+
targets: np.ndarray,
|
|
65
|
+
threshold: float,
|
|
66
|
+
rng: np.random.Generator,
|
|
67
|
+
scaler: float,
|
|
68
|
+
) -> int:
|
|
69
|
+
"""
|
|
70
|
+
Randomly revive compartments whose relative volume (element of
|
|
71
|
+
:paramref:`~revive_compartments_by_random.Js`) is smaller than
|
|
72
|
+
:paramref:`~revive_compartments_by_random.threshold`. The revived values are randomly
|
|
73
|
+
and uniformly sampled between the extreme values of :paramref:`targets` across all
|
|
74
|
+
compartments. The range can be scaled by the parameter :paramref:`scaler`. Note that
|
|
75
|
+
this function does not conserve the quantities in :paramref:`targets` across all
|
|
76
|
+
compartments, since the new values are randomly generated.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
Js:
|
|
80
|
+
Mutable. 1D array with the size of :math:`M`, containing values to determine
|
|
81
|
+
whether a compartment is dead.
|
|
82
|
+
targets:
|
|
83
|
+
Mutable. 2D array with the size of :math:`N_\\mathrm{c} \\times M`, containing
|
|
84
|
+
the values to be revived. The second dimension has to be the same as that of
|
|
85
|
+
Js. Note that this is not checked.
|
|
86
|
+
threshold:
|
|
87
|
+
Const. The threshold of :paramref:`Js` for a compartment to be considered
|
|
88
|
+
dead. For each element of :paramref:`Js` smaller than this parameter, the
|
|
89
|
+
corresponding compartment will be considered as dead, and its targets values
|
|
90
|
+
will then be randomly drawn between the corresponding minimum and maximum
|
|
91
|
+
values of :paramref:`targets` across all compartments. Corresponding
|
|
92
|
+
:paramref:`Js` will be set to be unity.
|
|
93
|
+
rng:
|
|
94
|
+
Mutable. Random number generator for reviving.
|
|
95
|
+
scaler:
|
|
96
|
+
Const. The scaler for generating random new values.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
: Number of dead compartments that have been revived.
|
|
100
|
+
"""
|
|
101
|
+
revive_count = 0
|
|
102
|
+
num_component, num_compartment = targets.shape
|
|
103
|
+
|
|
104
|
+
target_centers = np.full(num_component, 0.0, float)
|
|
105
|
+
omega_widths = np.full(num_component, 0.0, float)
|
|
106
|
+
for itr_component in range(num_component):
|
|
107
|
+
current_target_max = targets[itr_component].max()
|
|
108
|
+
current_target_min = targets[itr_component].min()
|
|
109
|
+
target_centers[itr_component] = (current_target_max + current_target_min) * 0.5
|
|
110
|
+
omega_widths[itr_component] = (current_target_max - current_target_min) * 0.5
|
|
111
|
+
|
|
112
|
+
# revive the compartment with random conjugate field
|
|
113
|
+
for itr_compartment in range(num_compartment):
|
|
114
|
+
if Js[itr_compartment] <= threshold:
|
|
115
|
+
Js[itr_compartment] = 1.0
|
|
116
|
+
revive_count += 1
|
|
117
|
+
for itr_component in range(num_component):
|
|
118
|
+
targets[itr_component, itr_compartment] = target_centers[
|
|
119
|
+
itr_component
|
|
120
|
+
] + omega_widths[itr_component] * scaler * rng.uniform(-1, 1)
|
|
121
|
+
|
|
122
|
+
return revive_count
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@nb.njit()
|
|
126
|
+
def revive_compartments_by_copy(
|
|
127
|
+
Js: np.ndarray,
|
|
128
|
+
targets: np.ndarray,
|
|
129
|
+
threshold: float,
|
|
130
|
+
rng: np.random.Generator,
|
|
131
|
+
) -> int:
|
|
132
|
+
"""
|
|
133
|
+
Revive compartments whose relative volume (element of
|
|
134
|
+
:paramref:`~revive_compartments_by_copy.Js`) is smaller than
|
|
135
|
+
:paramref:`~revive_compartments_by_copy.threshold`. The revived values are randomly
|
|
136
|
+
copied from other living compartments. Note that this function conserves the
|
|
137
|
+
quantities in :paramref:`targets` across all compartments by modifying the volumes
|
|
138
|
+
:paramref:`Js` accordingly.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
Js:
|
|
142
|
+
Mutable. 1D array with the size of :math:`M`, containing values to determine
|
|
143
|
+
whether a compartment is dead.
|
|
144
|
+
targets:
|
|
145
|
+
Mutable. 2D array with the size of :math:`N_\\mathrm{c} \\times M`, containing the
|
|
146
|
+
values to be revived. The second dimension has to be the same as that of Js.
|
|
147
|
+
Note that this is not checked.
|
|
148
|
+
threshold:
|
|
149
|
+
Const. The threshold of :paramref:`Js` for a compartment to be considered
|
|
150
|
+
dead. For each element of :paramref:`Js` smaller than this parameter, the
|
|
151
|
+
corresponding compartment will be considered as dead, and its targets values
|
|
152
|
+
will then be randomly drawn between the corresponding minimum and maximum
|
|
153
|
+
values of :paramref:`targets` across all compartments. Corresponding
|
|
154
|
+
:paramref:`Js` will be set to be unity.
|
|
155
|
+
rng:
|
|
156
|
+
Mutable. Random number generator for reviving.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
: Number of revives
|
|
160
|
+
"""
|
|
161
|
+
revive_count = 0
|
|
162
|
+
num_components, num_compartments = targets.shape
|
|
163
|
+
|
|
164
|
+
dead_indexes = np.full(num_compartments, -1, dtype=np.int32)
|
|
165
|
+
dead_count = 0
|
|
166
|
+
living_nicely_indexes = np.full(num_compartments, -1, dtype=np.int32)
|
|
167
|
+
living_nicely_count = 0
|
|
168
|
+
for itr_compartment in range(num_compartments):
|
|
169
|
+
if Js[itr_compartment] > 2.0 * threshold:
|
|
170
|
+
living_nicely_indexes[living_nicely_count] = itr_compartment
|
|
171
|
+
living_nicely_count += 1
|
|
172
|
+
elif Js[itr_compartment] <= threshold:
|
|
173
|
+
dead_indexes[dead_count] = itr_compartment
|
|
174
|
+
dead_count += 1
|
|
175
|
+
|
|
176
|
+
for itr_dead in dead_indexes[:dead_count]:
|
|
177
|
+
while True:
|
|
178
|
+
pos_in_living = rng.integers(0, living_nicely_count)
|
|
179
|
+
ref_index = living_nicely_indexes[pos_in_living]
|
|
180
|
+
if Js[int(ref_index)] > 2.0 * threshold:
|
|
181
|
+
targets[:, itr_dead] = targets[:, ref_index]
|
|
182
|
+
new_J = 0.5 * Js[ref_index]
|
|
183
|
+
Js[itr_dead] = new_J
|
|
184
|
+
Js[ref_index] = new_J
|
|
185
|
+
living_nicely_indexes[living_nicely_count] = itr_dead
|
|
186
|
+
living_nicely_count += 1
|
|
187
|
+
revive_count += 1
|
|
188
|
+
break
|
|
189
|
+
else:
|
|
190
|
+
living_nicely_count -= 1
|
|
191
|
+
living_nicely_indexes[pos_in_living] = living_nicely_indexes[
|
|
192
|
+
living_nicely_count
|
|
193
|
+
]
|
|
194
|
+
living_nicely_indexes[living_nicely_count] = -1
|
|
195
|
+
return revive_count
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@nb.njit()
|
|
199
|
+
def calc_volume_fractions(
|
|
200
|
+
phis: np.ndarray,
|
|
201
|
+
Js: np.ndarray,
|
|
202
|
+
sizes: np.ndarray,
|
|
203
|
+
phi_means: np.ndarray,
|
|
204
|
+
omegas: np.ndarray,
|
|
205
|
+
masks: np.ndarray,
|
|
206
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
207
|
+
"""
|
|
208
|
+
Calculate the volume fractions of components :math:`\\phi_i^{(m)}` from the conjugate
|
|
209
|
+
fields :math:`\\omega_i^{(m)}`. The single molecular partition functions $Q_i$ and the
|
|
210
|
+
incompressibility :math:`\\sum_i \\phi_i^{(m)} - 1` are also calculated. Note that the
|
|
211
|
+
volume fractions :math:`\\phi_i^{(m)}` are returned through the parameter.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
phis:
|
|
215
|
+
Output. The 2D array with the size of :math:`N_\\mathrm{c} \\times M`,
|
|
216
|
+
containing the volume fractions :math:`\\phi_i^{(m)}`. The first dimension has
|
|
217
|
+
to be the same as :paramref:`sizes`. The second dimension has to be the same
|
|
218
|
+
as that of :paramref:`Js`. Note that these are not checked.
|
|
219
|
+
Js:
|
|
220
|
+
Const. The 1D array with the size of :math:`M`, containing the relative
|
|
221
|
+
volumes of compartments :math:`J_m`. Note that :paramref:`Js` must be
|
|
222
|
+
invariant under multiplication of :paramref:`masks`.
|
|
223
|
+
sizes:
|
|
224
|
+
Const. The 1D array with the size of :math:`M`, containing the relative
|
|
225
|
+
molecule volumes of the components :math:`l_i`.
|
|
226
|
+
phi_means:
|
|
227
|
+
Const. The 1D array with the size of :math:`N_\\mathrm{c}`, containing the
|
|
228
|
+
mean volume fractions of the components :math:`\\bar{\\phi}_i`.
|
|
229
|
+
omegas:
|
|
230
|
+
Const. The 2D array with the size of :math:`N_\\mathrm{c} \\times M`,
|
|
231
|
+
containing the conjugate field :math:`\\omega_i^{(m)}`. The first dimension
|
|
232
|
+
has to be the same as :paramref:`sizes`. The second dimension has to be the
|
|
233
|
+
same as that of :paramref:`Js`. Note that these are not checked.
|
|
234
|
+
masks:
|
|
235
|
+
Const. The 1D array with the size of :math:`M`, containing the masks of
|
|
236
|
+
compartments. See :meth:`make_valid_compartment_masks` for more information.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
[0]:
|
|
240
|
+
1D array with the size of :math:`N_\\mathrm{c}`, containing the single
|
|
241
|
+
molecular partition functions of components :math:`Q_i`.
|
|
242
|
+
[1]:
|
|
243
|
+
1D array with the size of :math:`M`, containing the incompressibility
|
|
244
|
+
:math:`\\sum_i \\phi_i^{(m)} - 1`.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
num_components, num_compartments = omegas.shape
|
|
248
|
+
Qs = np.full(num_components, 0.0, float)
|
|
249
|
+
incomp = np.full(num_compartments, -1.0, float)
|
|
250
|
+
total_Js = Js.sum()
|
|
251
|
+
for itr_comp in range(num_components):
|
|
252
|
+
phis[itr_comp] = np.exp(-omegas[itr_comp] * sizes[itr_comp])
|
|
253
|
+
Qs[itr_comp] = (phis[itr_comp] * Js).sum()
|
|
254
|
+
Qs[itr_comp] /= total_Js
|
|
255
|
+
factor = phi_means[itr_comp] / Qs[itr_comp]
|
|
256
|
+
phis[itr_comp] = factor * phis[itr_comp] * masks
|
|
257
|
+
incomp += phis[itr_comp]
|
|
258
|
+
incomp *= masks
|
|
259
|
+
return Qs, incomp
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@nb.njit()
|
|
263
|
+
def multicomponent_self_consistent_metastep(
|
|
264
|
+
phi_means: np.ndarray,
|
|
265
|
+
chis: np.ndarray,
|
|
266
|
+
sizes: np.ndarray,
|
|
267
|
+
*,
|
|
268
|
+
omegas: np.ndarray,
|
|
269
|
+
Js: np.ndarray,
|
|
270
|
+
phis: np.ndarray,
|
|
271
|
+
steps_inner: int,
|
|
272
|
+
acceptance_Js: float,
|
|
273
|
+
Js_step_upper_bound: float,
|
|
274
|
+
acceptance_omega: float,
|
|
275
|
+
kill_threshold: float,
|
|
276
|
+
revive_tries: int,
|
|
277
|
+
revive_scaler: float,
|
|
278
|
+
rng: np.random.Generator,
|
|
279
|
+
) -> tuple[float, float, float, int, bool]:
|
|
280
|
+
"""
|
|
281
|
+
The core algorithm of finding coexisting states of multicomponent systems with
|
|
282
|
+
self-consistent iterations.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
phi_means:
|
|
286
|
+
Const. The interaction matrix. 2D array with size of :math:`N_\\mathrm{c}
|
|
287
|
+
\\times N_\\mathrm{c}`. This matrix should be the full :math:`\\chi_{ij}`
|
|
288
|
+
matrix of the system, including the solvent component. Note that the matrix
|
|
289
|
+
must be symmetric, which is not checked but should be guaranteed externally.
|
|
290
|
+
chis:
|
|
291
|
+
Const. The average volume fractions :math:`\\bar{\\phi}_i` of all the
|
|
292
|
+
components of the system. 1D array with size of :math:`N_\\mathrm{c}`. Note
|
|
293
|
+
that the volume fraction of the solvent is included as well, therefore the sum
|
|
294
|
+
of this array must be unity, which is not checked by this function and should
|
|
295
|
+
be guaranteed externally.
|
|
296
|
+
sizes:
|
|
297
|
+
Const. The relative molecule volumes :math:`l_i` of the components. 1D array
|
|
298
|
+
with size of :math:`N_\\mathrm{c}`. This sizes vector should be the full sizes
|
|
299
|
+
vector of the system, including the solvent component. An element of one
|
|
300
|
+
indicates that the corresponding specie has the same volume as the reference.
|
|
301
|
+
None indicates a all-one vector.
|
|
302
|
+
omegas:
|
|
303
|
+
Mutable. The conjugate field :math:`\\omega_i^{(m)}`. 2D array with size of
|
|
304
|
+
:math:`N_\\mathrm{c} \\times M`. Note that this field is both used as input
|
|
305
|
+
and output. Note again that this function DO NOT initialize `omegas`, it
|
|
306
|
+
should be initialized externally, and usually a random initialization will be
|
|
307
|
+
a reasonable choice.
|
|
308
|
+
Js:
|
|
309
|
+
Mutable. The relative volumes of the compartments :math:`J_m`. 1D array with
|
|
310
|
+
size of :math:`M`. The average value of `Js` will and should be unity. Note
|
|
311
|
+
that this field is both used as input and output. An all-one array is usually
|
|
312
|
+
a nice initialization, unless resume of a previous run is intended.
|
|
313
|
+
phis:
|
|
314
|
+
Output. The volume fractions :math:`\\phi_i^{(m)}`. 2D array with size of
|
|
315
|
+
:math:`N_\\mathrm{c} \\times M`.
|
|
316
|
+
steps_inner:
|
|
317
|
+
Constant. Number of steps in current routine. Within these steps, convergence
|
|
318
|
+
is not checked and no output will be generated.
|
|
319
|
+
acceptance_Js:
|
|
320
|
+
Constant. The acceptance of :paramref:`Js` (the relative compartment size
|
|
321
|
+
:math:`J_m`). This value determines the amount of changes accepted in each
|
|
322
|
+
step for the :math:`J_m` field. Typically this value can take the order of
|
|
323
|
+
:math:`10^{-3}`, or smaller when the system becomes larger or stiffer.
|
|
324
|
+
Js_step_upper_bound:
|
|
325
|
+
Constant. The maximum change of :paramref:`Js` (the relative compartment size
|
|
326
|
+
:math:`J_m`) per step. This value is designed to reduce the risk that a the
|
|
327
|
+
volume of a compartment changes too fast before it develops meaningful
|
|
328
|
+
composition. If the intended change is larger this value, all the changes will
|
|
329
|
+
be scaled down to guarantee that the maximum changes do not exceed this value.
|
|
330
|
+
Typically this value can take the order of :math:`10^{-3}`, or smaller when
|
|
331
|
+
the system becomes larger or stiffer.
|
|
332
|
+
acceptance_omega:
|
|
333
|
+
Constant. The acceptance of :paramref:`omegas`(the conjugate fields
|
|
334
|
+
:math:`\\omega_i^{(m)}`). This value determines the amount of changes accepted
|
|
335
|
+
in each step for the :math:`\\omega_i^{(m)}` field. Note that if the iteration
|
|
336
|
+
of :math:`J_m` is scaled down due to parameter
|
|
337
|
+
:paramref:`Js_step_upper_bound`, the iteration of :math:`\\omega_i^{(m)}`
|
|
338
|
+
fields will be scaled down simultaneously. Typically this value can take the
|
|
339
|
+
order of :math:`10^{-2}`, or smaller when the system becomes larger or
|
|
340
|
+
stiffer.
|
|
341
|
+
kill_threshold:
|
|
342
|
+
Constant. The threshold of the :math:`J_m` for a compartment to be considered
|
|
343
|
+
dead and killed afterwards. Should be not less than 0. In each iteration step,
|
|
344
|
+
the :math:`J_m` array will be checked, for each element smaller than this
|
|
345
|
+
parameter, the corresponding compartment will be killed and 0 will be assigned
|
|
346
|
+
to the internal mask. The dead compartment may be revived, depending whether
|
|
347
|
+
reviving is allowed or whether the number of the revive tries has been
|
|
348
|
+
exhausted.
|
|
349
|
+
revive_tries:
|
|
350
|
+
Constant. Number of tries left to revive the dead compartment. 0 or negative
|
|
351
|
+
value indicates no reviving. When this value is exhausted, i.e. the number of
|
|
352
|
+
revive in current function call exceeds this value, the revive will be turned
|
|
353
|
+
off. Note that this function does not decrease this value, but returns the number
|
|
354
|
+
of revives that have happened after completion.
|
|
355
|
+
revive_scaler:
|
|
356
|
+
Constant. The scaling factor for the conjugate fields :math:`\\omega_i^{(m)}`
|
|
357
|
+
when a dead compartment is revived. This value determines the range of the
|
|
358
|
+
random conjugate field generated by the algorithm. Typically 1.0 or some value
|
|
359
|
+
slightly larger will be a reasonable choice. See
|
|
360
|
+
:meth:`revive_compartments_by_random` for more information.
|
|
361
|
+
rng:
|
|
362
|
+
Mutable. random number generator for reviving.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
[0]: Max absolute incompressibility.
|
|
366
|
+
[1]: Max absolute conjugate field error.
|
|
367
|
+
[2]: Max absolute relative volumes error.
|
|
368
|
+
[3]: Number of revives.
|
|
369
|
+
[4]: Whether no phase is killed in the last step.
|
|
370
|
+
"""
|
|
371
|
+
num_components, num_compartments = omegas.shape
|
|
372
|
+
chi_sum_sum = chis.sum()
|
|
373
|
+
|
|
374
|
+
n_valid_phase = 0
|
|
375
|
+
|
|
376
|
+
revive_count = 0
|
|
377
|
+
for _ in range(steps_inner):
|
|
378
|
+
# check if we are still allowed to revive compartments
|
|
379
|
+
if revive_count < revive_tries:
|
|
380
|
+
n_valid_phase = count_valid_compartments(Js, kill_threshold)
|
|
381
|
+
if n_valid_phase != num_compartments:
|
|
382
|
+
# revive dead compartments
|
|
383
|
+
revive_count += revive_compartments_by_random(
|
|
384
|
+
Js, omegas, kill_threshold, rng, revive_scaler
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# generate masks for the compartments
|
|
388
|
+
masks = make_valid_compartment_masks(Js, kill_threshold)
|
|
389
|
+
n_valid_phase = int(masks.sum())
|
|
390
|
+
Js *= masks
|
|
391
|
+
|
|
392
|
+
# calculate volume fractions, single molecular partition function Q and incompressibility
|
|
393
|
+
(Qs, incomp) = calc_volume_fractions(phis, Js, sizes, phi_means, omegas, masks)
|
|
394
|
+
max_abs_incomp = np.abs(incomp).max()
|
|
395
|
+
|
|
396
|
+
# temp for omega, namely chi.phi
|
|
397
|
+
omega_temp = chis @ phis
|
|
398
|
+
|
|
399
|
+
# xi, the lagrangian multiplier
|
|
400
|
+
xi = chi_sum_sum * incomp
|
|
401
|
+
for itr_comp in range(num_components):
|
|
402
|
+
xi += omegas[itr_comp] - omega_temp[itr_comp]
|
|
403
|
+
xi *= masks
|
|
404
|
+
xi /= num_components
|
|
405
|
+
|
|
406
|
+
# local energy. i.e. energy of phases excluding the partition function part
|
|
407
|
+
local_energy = xi * incomp
|
|
408
|
+
for itr_comp in range(num_components):
|
|
409
|
+
local_energy += (
|
|
410
|
+
-0.5 * omega_temp[itr_comp] - xi - 1.0 / sizes[itr_comp]
|
|
411
|
+
) * phis[itr_comp]
|
|
412
|
+
|
|
413
|
+
# calculate the difference of Js
|
|
414
|
+
local_energy_mean = (local_energy * Js).sum() / n_valid_phase
|
|
415
|
+
Js_diff = (local_energy_mean - local_energy) * masks
|
|
416
|
+
max_abs_Js_diff = np.abs(Js_diff).max()
|
|
417
|
+
|
|
418
|
+
# calculate additional factor to scale down iteration
|
|
419
|
+
Js_max_change = max_abs_Js_diff * acceptance_Js
|
|
420
|
+
additional_factor = Js_step_upper_bound / max(Js_max_change, Js_step_upper_bound)
|
|
421
|
+
|
|
422
|
+
# update Js
|
|
423
|
+
Js += additional_factor * acceptance_Js * Js_diff
|
|
424
|
+
Js *= masks
|
|
425
|
+
Js += 1 - (Js.sum() / n_valid_phase)
|
|
426
|
+
Js *= masks
|
|
427
|
+
|
|
428
|
+
# calculate difference of omega and update omega directly
|
|
429
|
+
max_abs_omega_diff = 0
|
|
430
|
+
for itr_comp in range(num_components):
|
|
431
|
+
omega_temp[itr_comp] = omega_temp[itr_comp] + xi - omegas[itr_comp]
|
|
432
|
+
omega_temp[itr_comp] *= masks
|
|
433
|
+
omega_temp[itr_comp] -= omega_temp[itr_comp].sum() / n_valid_phase
|
|
434
|
+
max_abs_omega_diff = max(max_abs_omega_diff, omega_temp[itr_comp].max())
|
|
435
|
+
omegas[itr_comp] += (
|
|
436
|
+
additional_factor * acceptance_omega * omega_temp[itr_comp]
|
|
437
|
+
)
|
|
438
|
+
omegas[itr_comp] *= masks
|
|
439
|
+
omegas[itr_comp] -= omegas[itr_comp].sum() / n_valid_phase
|
|
440
|
+
|
|
441
|
+
# count the valid phases in the last step
|
|
442
|
+
n_valid_phase_last = count_valid_compartments(Js, kill_threshold)
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
max_abs_incomp,
|
|
446
|
+
max_abs_omega_diff,
|
|
447
|
+
max_abs_Js_diff,
|
|
448
|
+
revive_count,
|
|
449
|
+
n_valid_phase == n_valid_phase_last,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def sort_phases(
|
|
454
|
+
Js_phases: np.ndarray, phis_phases: np.ndarray
|
|
455
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
456
|
+
"""
|
|
457
|
+
Sort the phases according to the index of most concentrated components. Note that this
|
|
458
|
+
function uses different data structure from other functions in the module. See
|
|
459
|
+
:paramref:`phis_phases`.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
Js_phases:
|
|
463
|
+
Const. 1D array with the size of :math:`N_\\mathrm{p}`, containing the volumes
|
|
464
|
+
of each phase.
|
|
465
|
+
phis_phases:
|
|
466
|
+
Const. 2D array with the size of :math:`N_\\mathrm{p} \\times N_\\mathrm{c}`,
|
|
467
|
+
containing the volume fractions of the components in each phase. The first
|
|
468
|
+
dimension must be the same as :paramref:`Js_phases`. Note that this usually
|
|
469
|
+
corresponds to the transpose of the arrays of :math:`\\phi_i^{(m)}`, for
|
|
470
|
+
example :attr:`~flory.mcmp.CoexistingPhasesFinder.phis` in class
|
|
471
|
+
:class:`~flory.mcmp.CoexistingPhasesFinder`.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
[0]: Sorted :paramref:`Js_phases`.
|
|
475
|
+
[1]: Sorted :paramref:`phis_phases`.
|
|
476
|
+
"""
|
|
477
|
+
enrich_indexes = np.argsort(phis_phases)
|
|
478
|
+
sorting_index = np.lexsort(np.transpose(enrich_indexes))
|
|
479
|
+
return Js_phases[sorting_index], phis_phases[sorting_index]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def get_clusters(
|
|
483
|
+
Js: np.ndarray, phis: np.ndarray, dist: float = 1e-2
|
|
484
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
485
|
+
"""
|
|
486
|
+
Find unique phases from compartments by clustering. The returning results are sorted
|
|
487
|
+
according to the index of most concentrated components.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
Js:
|
|
491
|
+
Const. 1D array with the size of :math:`M`, containing the volumes of each
|
|
492
|
+
compartment.
|
|
493
|
+
phis:
|
|
494
|
+
Const. 2D array with the size of :math:`N_\\mathrm{c} \\times M`, containing
|
|
495
|
+
the volume fractions of the components in each phase :math:`\\phi_i^{(m)}`.
|
|
496
|
+
The second dimension must be the same as :math:`Js`.
|
|
497
|
+
dist:
|
|
498
|
+
Cut-off distance for cluster analysis
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
[0]:
|
|
502
|
+
1D array with the size of :math:`N_\\mathrm{p}`, containing the volumes of the
|
|
503
|
+
unique phases.
|
|
504
|
+
[1]:
|
|
505
|
+
1D array with the size of :math:`N_\\mathrm{p} \\times N_\\mathrm{c}`,
|
|
506
|
+
containing the compositions of all unique phases. Note that the data structure
|
|
507
|
+
is different from normal, see :meth:`sort_phases` for more information.
|
|
508
|
+
"""
|
|
509
|
+
# transpose to make the compartment index the first index
|
|
510
|
+
phis = np.transpose(phis)
|
|
511
|
+
# calculate distances between compositions
|
|
512
|
+
if phis.shape[0] == 1:
|
|
513
|
+
return phis, 1
|
|
514
|
+
dists = spatial.distance.pdist(phis)
|
|
515
|
+
# obtain hierarchy structure
|
|
516
|
+
links = cluster.hierarchy.linkage(dists, method="centroid")
|
|
517
|
+
# flatten the hierarchy by clustering
|
|
518
|
+
clusters = cluster.hierarchy.fcluster(links, dist, criterion="distance")
|
|
519
|
+
cluster_phis = np.array(
|
|
520
|
+
[phis[clusters == n, :].mean(axis=0) for n in np.unique(clusters)]
|
|
521
|
+
)
|
|
522
|
+
cluster_Js = np.array([Js[clusters == n].sum(axis=0) for n in np.unique(clusters)])
|
|
523
|
+
cluster_Js /= cluster_Js.sum()
|
|
524
|
+
return sort_phases(cluster_Js, cluster_phis)
|
flory/mcmp.py
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""Module for finding coexisting phases of multicomponent mixtures.
|
|
2
|
+
|
|
3
|
+
:mod:`flory.mcmp` provides tools for finding equilibrium multiple coexisting phases in
|
|
4
|
+
multicomponent mixtures in the canonical ensemble based on Flory-Huggins theory. The
|
|
5
|
+
module provides both the function API :meth:`find_coexisting_phases` and the class API
|
|
6
|
+
:class:`CoexistingPhasesFinder`. The function :meth:`find_coexisting_phases` aims at
|
|
7
|
+
easing the calculation of the coexisting phases with a single system setting, namely one
|
|
8
|
+
single point in the phase diagram. In contrast, The class :class:`CoexistingPhasesFinder`
|
|
9
|
+
is designed for reuse over systems with the same system sizes such as the number of
|
|
10
|
+
components, which includes generating a coexisting curve or sampling a phase diagram. It
|
|
11
|
+
creates a finder to maintain its internal data, and provides more control and diagnostics
|
|
12
|
+
over the iteration. See :ref:`Examples` for examples.
|
|
13
|
+
|
|
14
|
+
.. codeauthor:: Yicheng Qiang <yicheng.qiang@ds.mpg.de>
|
|
15
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import numpy as np
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Optional
|
|
22
|
+
from tqdm.auto import tqdm
|
|
23
|
+
from .detail.mcmp_impl import *
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CoexistingPhasesFinder:
|
|
27
|
+
"""
|
|
28
|
+
Create a finder for coexisting phases.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
chis: np.ndarray,
|
|
34
|
+
phi_means: np.ndarray,
|
|
35
|
+
num_compartments: int,
|
|
36
|
+
*,
|
|
37
|
+
sizes: Optional[np.ndarray] = None,
|
|
38
|
+
rng: Optional[np.random.Generator] = None,
|
|
39
|
+
max_steps: int = 1000000,
|
|
40
|
+
convergence_criterion: str = "standard",
|
|
41
|
+
tolerance: float = 1e-5,
|
|
42
|
+
interval: int = 10000,
|
|
43
|
+
progress: bool = True,
|
|
44
|
+
random_std: float = 5.0,
|
|
45
|
+
acceptance_Js: float = 0.0002,
|
|
46
|
+
acceptance_omega: float = 0.002,
|
|
47
|
+
Js_step_upper_bound: float = 0.001,
|
|
48
|
+
kill_threshold: float = 0.0,
|
|
49
|
+
revive_scaler: float = 1.0,
|
|
50
|
+
max_revive_per_compartment: int = 16,
|
|
51
|
+
additional_chis_shift: float = 1.0,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Construct a :class:`CoexistingPhasesFinder` instance for finding coexisting
|
|
55
|
+
phases. This class is recommended when multiple instances of :paramref:`chis`
|
|
56
|
+
matrix or :paramref:`phi_means` vector need to be calculated. The class will reuse
|
|
57
|
+
all the options and the internal resources. Note that reuse the instance of this
|
|
58
|
+
class is only possible when all the system sizes are not changed, including the
|
|
59
|
+
number of components :math:`N_\\mathrm{c}` and the number of compartments
|
|
60
|
+
:math:`M`. The the number of components :math:`N_\\mathrm{c}` is inferred from
|
|
61
|
+
:paramref:`chis` and :paramref:`phi_means`, while :math:`N_\\mathrm{c}` is set by
|
|
62
|
+
the parameter :paramref:`num_compartments`. Setting :paramref:`chis` matrix and
|
|
63
|
+
:paramref:`phi_means` manually by the setters leads to the reset of the internal
|
|
64
|
+
revive counters.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
chis:
|
|
68
|
+
The interaction matrix. 2D array with size of :math:`N_\\mathrm{c} \\times
|
|
69
|
+
N_\\mathrm{c}`. This matrix should be the full :math:`\\chi_{ij}` matrix
|
|
70
|
+
of the system, including the solvent component. Note that the matrix must
|
|
71
|
+
be symmetric, which is not checked but should be guaranteed externally.
|
|
72
|
+
phi_means:
|
|
73
|
+
The average volume fractions :math:`\\bar{\\phi}_i` of all the components
|
|
74
|
+
of the system. 1D array with size of :math:`N_\\mathrm{c}`. Note that the
|
|
75
|
+
volume fraction of the solvent is included as well, therefore the sum of
|
|
76
|
+
this array must be unity, which is not checked by this function and should
|
|
77
|
+
be guaranteed externally.
|
|
78
|
+
num_compartments:
|
|
79
|
+
Number of compartments :math:`M` in the system.
|
|
80
|
+
sizes:
|
|
81
|
+
The relative molecule volumes :math:`l_i` of the components. 1D array with
|
|
82
|
+
size of :math:`N_\\mathrm{c}`. This sizes vector should be the full sizes
|
|
83
|
+
vector of the system, including the solvent component. An element of one
|
|
84
|
+
indicates that the corresponding specie has the same volume as the
|
|
85
|
+
reference. None indicates a all-one vector.
|
|
86
|
+
rng:
|
|
87
|
+
Random number generator for initialization and reviving. None indicates
|
|
88
|
+
that a new random number generator should be created by the class, seeded
|
|
89
|
+
by current timestamp.
|
|
90
|
+
max_steps:
|
|
91
|
+
The default maximum number of steps in each run to find the coexisting
|
|
92
|
+
phases. This value can be temporarily overwritten, see :meth:`run` for more
|
|
93
|
+
information.
|
|
94
|
+
convergence_criterion:
|
|
95
|
+
The criterion to determine convergence. Currently "standard" is the only
|
|
96
|
+
option, which requires checking of incompressibility, field error and the
|
|
97
|
+
volume error. Note that all these metrics are state functions, namely they
|
|
98
|
+
are independent of iteration parameters.
|
|
99
|
+
tolerance:
|
|
100
|
+
The tolerance to determine convergence. This value can be temporarily
|
|
101
|
+
overwritten. See :paramref:`convergence_criterion` and :meth:`run` for
|
|
102
|
+
more information.
|
|
103
|
+
interval:
|
|
104
|
+
The interval of steps to check convergence. This value can be temporarily
|
|
105
|
+
overwritten, see :meth:`run` for more information.
|
|
106
|
+
progress:
|
|
107
|
+
Whether to show progress bar when checking convergence. This value can be
|
|
108
|
+
temporarily overwritten, see :meth:`run` for more information.
|
|
109
|
+
random_std:
|
|
110
|
+
The amplitude of the randomly generated conjugate fields internally.
|
|
111
|
+
During instantiation, the conjugate field is randomly generated according
|
|
112
|
+
to an normal distribution with standard deviation :paramref:`random_std`.
|
|
113
|
+
acceptance_Js:
|
|
114
|
+
The acceptance of the relative compartment size :math:`J_m`. This value
|
|
115
|
+
determines the amount of changes accepted in each step for the :math:`J_m`
|
|
116
|
+
field. Typically this value can take the order of :math:`10^{-3}`, or
|
|
117
|
+
smaller when the system becomes larger or stiffer.
|
|
118
|
+
Js_step_upper_bound:
|
|
119
|
+
The maximum change of the relative compartment size :math:`J_m` per step.
|
|
120
|
+
This value is designed to reduce the risk that a the volume of a
|
|
121
|
+
compartment changes too fast before it develops meaningful composition. If
|
|
122
|
+
the intended change is larger this value, all the changes will be scaled
|
|
123
|
+
down to guarantee that the maximum changes do not exceed this value.
|
|
124
|
+
Typically this value can take the order of :math:`10^{-3}`, or smaller
|
|
125
|
+
when the system becomes larger or stiffer.
|
|
126
|
+
acceptance_omega:
|
|
127
|
+
The acceptance of the conjugate fields :math:`\\omega_i^{(m)}`. This value
|
|
128
|
+
determines the amount of changes accepted in each step for the
|
|
129
|
+
:math:`\\omega_i^{(m)}` field. Note that if the iteration of :math:`J_m` is
|
|
130
|
+
scaled down due to parameter :paramref:`Js_step_upper_bound`, the
|
|
131
|
+
iteration of :math:`\\omega_i^{(m)}` fields will be scaled down simultaneously.
|
|
132
|
+
Typically this value can take the order of :math:`10^{-2}`, or smaller
|
|
133
|
+
when the system becomes larger or stiffer.
|
|
134
|
+
kill_threshold:
|
|
135
|
+
The threshold of the :math:`J_m` for a compartment to be considered dead
|
|
136
|
+
and killed afterwards. Should be not less than 0. In each iteration step,
|
|
137
|
+
the :math:`J_m` array will be checked, for each element smaller than this
|
|
138
|
+
parameter, the corresponding compartment will be killed and 0 will be
|
|
139
|
+
assigned to the internal mask. The dead compartment may be revived,
|
|
140
|
+
depending whether reviving is allowed or whether the number of the revive
|
|
141
|
+
tries has been exhausted.
|
|
142
|
+
revive_scaler:
|
|
143
|
+
The scaler for the value of the newly-generated conjugate fields when a
|
|
144
|
+
dead compartment is revived. The compartment is revived by drawing random
|
|
145
|
+
numbers for their conjugate fields in the range of the minimum and the
|
|
146
|
+
maximum of the :math:`\\omega_i^{(m)}` their conjugate fields across all
|
|
147
|
+
compartments. This value determines whether this range should be enlarged
|
|
148
|
+
(a value larger than 1) or reduced (a value smaller than 1). Typically 1.0
|
|
149
|
+
or a value slightly larger than 1.0 will be a reasonable choice.
|
|
150
|
+
max_revive_per_compartment:
|
|
151
|
+
Maximum average number of tries per compartment to revive the dead
|
|
152
|
+
compartments. 0 or negative value indicates no reviving. When this value
|
|
153
|
+
is exhausted, the revive will be turned off.
|
|
154
|
+
additional_chis_shift:
|
|
155
|
+
Shift of the entire chis matrix to improve the convergence by evolving
|
|
156
|
+
towards incompressible system faster. This value should be larger than 0.
|
|
157
|
+
Note that with very large value, the convergence will be slowed down,
|
|
158
|
+
since the algorithm no longer have enough ability to temporarily relax the
|
|
159
|
+
incompressibility.
|
|
160
|
+
"""
|
|
161
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
|
162
|
+
|
|
163
|
+
# chis
|
|
164
|
+
chis = np.array(chis)
|
|
165
|
+
if chis.shape[0] == chis.shape[1]:
|
|
166
|
+
self._chis = chis
|
|
167
|
+
self._num_components = chis.shape[0]
|
|
168
|
+
self._logger.info(
|
|
169
|
+
f"We infer that there are {self._num_components} components in the system from the chis matrix."
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
self._logger.error(f"chis matrix with size of {chis.shape} is not square.")
|
|
173
|
+
raise ValueError("chis matrix must be square.")
|
|
174
|
+
if (chis != chis.transpose()).any():
|
|
175
|
+
self._logger.error(f"chis matrix is not symmetric.")
|
|
176
|
+
raise ValueError("chis matrix must be symmetric.")
|
|
177
|
+
|
|
178
|
+
# phi_means
|
|
179
|
+
phi_means = np.array(phi_means)
|
|
180
|
+
if phi_means.shape[0] == self._num_components:
|
|
181
|
+
self._phi_means = phi_means
|
|
182
|
+
if np.abs(self._phi_means.sum() - 1.0) > 1e-12:
|
|
183
|
+
self._logger.warning(
|
|
184
|
+
f"Total phi_means is not 1.0. Iteration may never converge."
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
self._logger.error(
|
|
188
|
+
f"phi_means vector with size of {phi_means.shape} is invalid, since {self._num_components} is defined by chis matrix."
|
|
189
|
+
)
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"phi_means vector must imply same component number as chis matrix."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self._num_compartments = num_compartments
|
|
195
|
+
|
|
196
|
+
## optional arguments
|
|
197
|
+
|
|
198
|
+
# sizes
|
|
199
|
+
if sizes is None:
|
|
200
|
+
self._sizes = np.ones(self._num_components)
|
|
201
|
+
else:
|
|
202
|
+
sizes = np.array(sizes)
|
|
203
|
+
if sizes.shape[0] == self._num_components:
|
|
204
|
+
self._sizes = sizes
|
|
205
|
+
if np.sum(self._sizes <= 0):
|
|
206
|
+
self._logger.warning(
|
|
207
|
+
f"Non-positive sizes detected. Iteration will probably fail."
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
self._logger.error(
|
|
211
|
+
f"sizes vector with size of {sizes.shape} is invalid, since {self._num_components} is defined by chis matrix."
|
|
212
|
+
)
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"sizes vector must imply same component number as chis matrix."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# rng
|
|
218
|
+
if rng is None:
|
|
219
|
+
self._rng_is_external = False
|
|
220
|
+
self._rng_seed = int(datetime.now().timestamp())
|
|
221
|
+
self._rng = np.random.default_rng(self._rng_seed)
|
|
222
|
+
else:
|
|
223
|
+
self._rng_is_external = True
|
|
224
|
+
self._rng_seed = int(0)
|
|
225
|
+
self._rng = rng
|
|
226
|
+
|
|
227
|
+
# other parameters
|
|
228
|
+
self._max_steps = max_steps
|
|
229
|
+
self._convergence_criterion = convergence_criterion
|
|
230
|
+
self._tolerance = tolerance
|
|
231
|
+
self._interval = interval
|
|
232
|
+
self._progress = progress
|
|
233
|
+
|
|
234
|
+
self._random_std = random_std
|
|
235
|
+
self._acceptance_Js = acceptance_Js
|
|
236
|
+
self._acceptance_omega = acceptance_omega
|
|
237
|
+
self._Js_step_upper_bound = Js_step_upper_bound
|
|
238
|
+
self._kill_threshold = kill_threshold
|
|
239
|
+
self._revive_scaler = revive_scaler
|
|
240
|
+
self._max_revive_per_compartment = max_revive_per_compartment
|
|
241
|
+
self._additional_chis_shift = additional_chis_shift
|
|
242
|
+
|
|
243
|
+
# diagnostics
|
|
244
|
+
self._diagnostics = {}
|
|
245
|
+
|
|
246
|
+
## initialize derived internal states
|
|
247
|
+
self._Js = np.full(self._num_compartments, 0.0, float)
|
|
248
|
+
self._omegas = np.full((self._num_components, self._num_compartments), 0.0, float)
|
|
249
|
+
self._phis = np.full((self._num_components, self._num_compartments), 0.0, float)
|
|
250
|
+
self._revive_count_left = (
|
|
251
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
252
|
+
)
|
|
253
|
+
self.reinitialize_random()
|
|
254
|
+
|
|
255
|
+
def reinitialize_random(self):
|
|
256
|
+
"""
|
|
257
|
+
Reinitialize the internal conjugate field :math:`\\omega_i^{(m)}` randomly. See parameter
|
|
258
|
+
:paramref:`CoexistingPhasesFinder.random_std` for more information.
|
|
259
|
+
"""
|
|
260
|
+
self._omegas = self._rng.normal(
|
|
261
|
+
0.0,
|
|
262
|
+
self._random_std,
|
|
263
|
+
(self._num_components, self._num_compartments),
|
|
264
|
+
)
|
|
265
|
+
self._Js = np.full(self._num_compartments, 1.0, float)
|
|
266
|
+
self._revive_count_left = (
|
|
267
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def reinitialize_from_omegas(self, omegas: np.ndarray):
|
|
271
|
+
"""
|
|
272
|
+
Reinitialize the internal conjugate field :math:`\\omega_i^{(m)}` from input.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
omegas:
|
|
276
|
+
New :math:`\\omega_i^{(m)}` field, must have the same size of
|
|
277
|
+
:math:`N_\\mathrm{c} \\times M`.
|
|
278
|
+
"""
|
|
279
|
+
omegas = np.array(omegas)
|
|
280
|
+
if omegas.shape == self._omegas.shape:
|
|
281
|
+
self._omegas = omegas
|
|
282
|
+
else:
|
|
283
|
+
self._logger.error(
|
|
284
|
+
f"new omegas with size of {omegas.shape} is invalid. It must have the size of {(self._num_components, self._num_compartments)}."
|
|
285
|
+
)
|
|
286
|
+
raise ValueError("New omegas must match the size of the old one.")
|
|
287
|
+
self._Js = np.ones_like(self._Js)
|
|
288
|
+
self._revive_count_left = (
|
|
289
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def reinitialize_from_phis(self, phis):
|
|
293
|
+
"""
|
|
294
|
+
Reinitialize the internal conjugate field :math:`\\omega_i^{(m)}` from volume fraction
|
|
295
|
+
field :math:`\\phi_i^{(m)}`. Note that it is not guaranteed that the initial volume
|
|
296
|
+
fraction field :math:`\\phi_i^{(m)}` is fully respected. The input is only considered
|
|
297
|
+
as a suggestion for the generation of :math:`\\omega_i^{(m)}` field.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
phis:
|
|
301
|
+
New :math:`\\phi_i^{(m)}` field, must have the same size of :math:`N_\\mathrm{c}
|
|
302
|
+
\\times M`.
|
|
303
|
+
"""
|
|
304
|
+
if phis.shape == self._omegas.shape:
|
|
305
|
+
self._omegas = -np.log(phis)
|
|
306
|
+
for itr in range(self._num_components):
|
|
307
|
+
self._omegas[itr] /= self._sizes[itr]
|
|
308
|
+
else:
|
|
309
|
+
self._logger.error(
|
|
310
|
+
f"phis with size of {phis.shape} is invalid. It must have the size of {(self._num_components, self._num_compartments)}."
|
|
311
|
+
)
|
|
312
|
+
raise ValueError("phis must match the size of the omegas.")
|
|
313
|
+
self._Js = np.ones_like(self._Js)
|
|
314
|
+
self._revive_count_left = (
|
|
315
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def chis(self) -> np.ndarray:
|
|
320
|
+
"""
|
|
321
|
+
The full interaction matrix :math:`\\chi_{ij}`, with the size of
|
|
322
|
+
:math:`N_\\mathrm{c} \\times N_\\mathrm{c}`. Resetting this property requires
|
|
323
|
+
that the new matrix has the same size with the internal one. Note that this
|
|
324
|
+
implies implicit reset of the internal data, the number of revives, but not the
|
|
325
|
+
others including volume fractions :math:`\\phi_i^{(m)}` and conjugate fields
|
|
326
|
+
:math:`\\omega_i^{(m)}`. See class parameters
|
|
327
|
+
:paramref:`~CoexistingPhasesFinder.chis` and
|
|
328
|
+
:paramref:`~CoexistingPhasesFinder.max_revive_per_compartment` for more
|
|
329
|
+
information.
|
|
330
|
+
"""
|
|
331
|
+
return self._chis
|
|
332
|
+
|
|
333
|
+
@chis.setter
|
|
334
|
+
def chis(self, chis_new: np.ndarray):
|
|
335
|
+
chis_new = np.array(chis_new)
|
|
336
|
+
if chis_new.shape == self._chis.shape:
|
|
337
|
+
if (chis_new != chis_new.transpose()).any():
|
|
338
|
+
self._logger.error(f"chis matrix is not symmetric.")
|
|
339
|
+
raise ValueError("chis matrix must be symmetric.")
|
|
340
|
+
self._chis = chis_new
|
|
341
|
+
else:
|
|
342
|
+
self._logger.error(
|
|
343
|
+
f"new chis with size of {chis_new.shape} is invalid. It must have the size of {self._chis.shape}."
|
|
344
|
+
)
|
|
345
|
+
raise ValueError("New chis must match the size of the old one.")
|
|
346
|
+
self._revive_count_left = (
|
|
347
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def phi_means(self) -> np.ndarray:
|
|
352
|
+
"""
|
|
353
|
+
The average volume fractions of all components :math:`\\bar{\\phi}_i`, with the
|
|
354
|
+
size of :math:`N_\\mathrm{c}`. Resetting this property requires that the new array
|
|
355
|
+
has the same size with the internal one. Note that this implies implicit reset of
|
|
356
|
+
the internal data, the number of revives, but not the others including volume
|
|
357
|
+
fractions :math:`\\phi_i^{(m)}` and conjugate fields :math:`\\omega_i^{(m)}`. See
|
|
358
|
+
class parameters :paramref:`~CoexistingPhasesFinder.phis_mean` and
|
|
359
|
+
:paramref:`~CoexistingPhasesFinder.max_revive_per_compartment` for more
|
|
360
|
+
information.
|
|
361
|
+
"""
|
|
362
|
+
return self._phi_means
|
|
363
|
+
|
|
364
|
+
@phi_means.setter
|
|
365
|
+
def phi_means(self, phi_means_new):
|
|
366
|
+
phi_means_new = np.array(phi_means_new)
|
|
367
|
+
if phi_means_new.shape == self._phi_means.shape:
|
|
368
|
+
self._phi_means = phi_means_new
|
|
369
|
+
if np.abs(self._phi_means.sum() - 1.0) > 1e-12:
|
|
370
|
+
self._logger.warning(
|
|
371
|
+
f"Total phi_means is not 1.0. Iteration may never converge."
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
self._logger.error(
|
|
375
|
+
f"new phi_means with size of {phi_means_new.shape} is invalid. It must have the size of {self._phi_means.shape}."
|
|
376
|
+
)
|
|
377
|
+
raise ValueError("New phi_means must match the size of the old one.")
|
|
378
|
+
self._revive_count_left = (
|
|
379
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def sizes(self) -> np.ndarray:
|
|
384
|
+
"""
|
|
385
|
+
The relative molecule sizes of all components :math:`l_i` with the size of
|
|
386
|
+
:math:`N_\\mathrm{c}`. Resetting this property requires that the new array has
|
|
387
|
+
the same size with the internal one. Note that this implies implicit reset of the
|
|
388
|
+
internal data, the number of revives, but not the others including volume
|
|
389
|
+
fractions :math:`\\phi_i^{(m)}` and conjugate fields :math:`\\omega_i^{(m)}`. See
|
|
390
|
+
class parameters :paramref:`~CoexistingPhasesFinder.sizes` and
|
|
391
|
+
:paramref:`~CoexistingPhasesFinder.max_revive_per_compartment` for more
|
|
392
|
+
information.
|
|
393
|
+
"""
|
|
394
|
+
return self._sizes
|
|
395
|
+
|
|
396
|
+
@sizes.setter
|
|
397
|
+
def sizes(self, sizes_new):
|
|
398
|
+
sizes_new = np.array(sizes_new)
|
|
399
|
+
if sizes_new.shape == self._sizes.shape:
|
|
400
|
+
self._sizes = sizes_new
|
|
401
|
+
else:
|
|
402
|
+
self._logger.error(
|
|
403
|
+
f"new sizes with size of {sizes_new.shape} is invalid. It must have the size of {self._sizes.shape}."
|
|
404
|
+
)
|
|
405
|
+
raise ValueError("New sizes must match the size of the old one.")
|
|
406
|
+
self._revive_count_left = (
|
|
407
|
+
self._max_revive_per_compartment * self._num_compartments
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def phis(self) -> np.ndarray:
|
|
412
|
+
"""
|
|
413
|
+
The volume fractions field :math:`\\phi_i^{(m)}`, with the size of
|
|
414
|
+
:math:`N_\\mathrm{c} \\times M`. This property should not be reset in most cases.
|
|
415
|
+
Resetting this property also takes no effect, since :math:`\\phi_i^{(m)}` is not
|
|
416
|
+
considered as the master internal state of the algorithm. Any set values will be
|
|
417
|
+
ignored. If you intend to initialize the system from certain volume fractions,
|
|
418
|
+
please refer to :meth:`reinitialize_from_phis`.
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
return self._phis
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def omegas(self) -> np.ndarray:
|
|
425
|
+
"""
|
|
426
|
+
The internal conjugate field :math:`\\omega_i^{(m)}`, with the size of
|
|
427
|
+
:math:`N_\\mathrm{c} \\times M`. This property should not be reset in most cases.
|
|
428
|
+
Resetting this property will change the state of the system, but skip necessary
|
|
429
|
+
reset of other internal states. If you intend to initialize the system from a
|
|
430
|
+
custom conjugate field, please refer to :meth:`reinitialize_from_omegas`.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
return self._omegas
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def diagnostics(self) -> dict:
|
|
437
|
+
"""
|
|
438
|
+
The diagnostics of the most recent call of :meth:`run`. The diagnostics contain
|
|
439
|
+
the convergence status and the original volume fractions before the clustering and
|
|
440
|
+
sorting algorithm is utilized to determine the unique phases.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
return self._diagnostics
|
|
444
|
+
|
|
445
|
+
def run(
|
|
446
|
+
self,
|
|
447
|
+
*,
|
|
448
|
+
max_steps: Optional[float] = None,
|
|
449
|
+
tolerance: Optional[float] = None,
|
|
450
|
+
interval: Optional[int] = None,
|
|
451
|
+
progress: Optional[bool] = None,
|
|
452
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
453
|
+
"""
|
|
454
|
+
Run instance to find coexisting phases. The keywords arguments can be used to
|
|
455
|
+
temporarily overwrite the provided values during construction of the class. Note
|
|
456
|
+
that this temporary values will not affect the defaults. See class
|
|
457
|
+
:class:`CoexistingPhasesFinder` for other tunable parameters. After each call, the
|
|
458
|
+
diagnostics will be updated, which can be used to, for example, inspecting whether
|
|
459
|
+
the iteration really converges. See :attr:`diagnostics` for more information.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
max_steps:
|
|
463
|
+
The maximum number of steps to find the coexisting phases.
|
|
464
|
+
tolerance:
|
|
465
|
+
The tolerance to determine convergence. See
|
|
466
|
+
:paramref:`~CoexistingPhasesFinder.convergence_criterion` for more
|
|
467
|
+
information.
|
|
468
|
+
interval:
|
|
469
|
+
The interval of steps to check convergence.
|
|
470
|
+
progress:
|
|
471
|
+
Whether to show progress bar when checking convergence.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
[0]:
|
|
475
|
+
Volume fractions of each phase :math:`J_\\alpha`. 1D array with the size of
|
|
476
|
+
:math:`N_\\mathrm{p}`.
|
|
477
|
+
[1]:
|
|
478
|
+
Volume fractions of components in each phase :math:`\\phi_i^{(\\alpha)}`.
|
|
479
|
+
2D array with the size of :math:`N_\\mathrm{p} \\times N_\\mathrm{c}`.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
if max_steps is None:
|
|
483
|
+
max_steps = self._max_steps
|
|
484
|
+
if tolerance is None:
|
|
485
|
+
tolerance = self._tolerance
|
|
486
|
+
if interval is None:
|
|
487
|
+
interval = self._interval
|
|
488
|
+
if progress is None:
|
|
489
|
+
progress = self._progress
|
|
490
|
+
|
|
491
|
+
steps_tracker = int(np.ceil(max_steps / interval))
|
|
492
|
+
steps_inner = max(1, int(np.ceil(max_steps)) // steps_tracker)
|
|
493
|
+
|
|
494
|
+
steps = 0
|
|
495
|
+
|
|
496
|
+
chis_shifted = self._chis.copy()
|
|
497
|
+
chis_shifted += -self._chis.min() + self._additional_chis_shift
|
|
498
|
+
|
|
499
|
+
bar_max = -np.log10(tolerance)
|
|
500
|
+
bar_format = "{desc:<20}: {percentage:3.0f}%|{bar}{r_bar}"
|
|
501
|
+
bar_args = {
|
|
502
|
+
"total": bar_max,
|
|
503
|
+
"disable": not progress,
|
|
504
|
+
"bar_format": bar_format,
|
|
505
|
+
}
|
|
506
|
+
bar_text_args = {
|
|
507
|
+
"total": 1,
|
|
508
|
+
"disable": not progress,
|
|
509
|
+
"bar_format": "{desc}",
|
|
510
|
+
}
|
|
511
|
+
pbar1 = tqdm(**bar_args, position=0, desc="Incompressibility")
|
|
512
|
+
pbar2 = tqdm(**bar_args, position=1, desc="Field Error")
|
|
513
|
+
pbar3 = tqdm(**bar_args, position=2, desc="Volume Error")
|
|
514
|
+
pbar4 = tqdm(**bar_text_args, position=3, desc="Revive Count Left")
|
|
515
|
+
|
|
516
|
+
bar_val_func = lambda a: max(0, min(round(-np.log10(max(a, 1e-100)), 1), bar_max))
|
|
517
|
+
|
|
518
|
+
for _ in range(steps_tracker):
|
|
519
|
+
# do the inner steps
|
|
520
|
+
(
|
|
521
|
+
max_abs_incomp,
|
|
522
|
+
max_abs_omega_diff,
|
|
523
|
+
max_abs_Js_diff,
|
|
524
|
+
revive_count,
|
|
525
|
+
is_last_step_safe,
|
|
526
|
+
) = multicomponent_self_consistent_metastep(
|
|
527
|
+
self._phi_means,
|
|
528
|
+
chis_shifted,
|
|
529
|
+
self._sizes,
|
|
530
|
+
omegas=self._omegas,
|
|
531
|
+
Js=self._Js,
|
|
532
|
+
phis=self._phis,
|
|
533
|
+
steps_inner=steps_inner,
|
|
534
|
+
acceptance_Js=self._acceptance_Js,
|
|
535
|
+
Js_step_upper_bound=self._Js_step_upper_bound,
|
|
536
|
+
acceptance_omega=self._acceptance_omega,
|
|
537
|
+
kill_threshold=self._kill_threshold,
|
|
538
|
+
revive_tries=self._revive_count_left,
|
|
539
|
+
revive_scaler=self._revive_scaler,
|
|
540
|
+
rng=self._rng,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
steps += steps_inner
|
|
544
|
+
self._revive_count_left -= revive_count
|
|
545
|
+
|
|
546
|
+
if progress:
|
|
547
|
+
pbar1.n = bar_val_func(max_abs_incomp)
|
|
548
|
+
pbar2.n = bar_val_func(max_abs_omega_diff)
|
|
549
|
+
pbar3.n = bar_val_func(max_abs_Js_diff)
|
|
550
|
+
pbar4.n = 1
|
|
551
|
+
pbar1.refresh()
|
|
552
|
+
pbar2.refresh()
|
|
553
|
+
pbar3.refresh()
|
|
554
|
+
pbar4.set_description_str(
|
|
555
|
+
"{:<20}: {}".format("Revive Count Left", self._revive_count_left)
|
|
556
|
+
)
|
|
557
|
+
pbar4.refresh()
|
|
558
|
+
|
|
559
|
+
# check convergence
|
|
560
|
+
if self._convergence_criterion == "standard":
|
|
561
|
+
if (
|
|
562
|
+
is_last_step_safe
|
|
563
|
+
and tolerance > max_abs_incomp
|
|
564
|
+
and tolerance > max_abs_omega_diff
|
|
565
|
+
and tolerance > max_abs_Js_diff
|
|
566
|
+
):
|
|
567
|
+
self._logger.info(
|
|
568
|
+
f"Composition and volumes reached stationary state after {steps} steps"
|
|
569
|
+
)
|
|
570
|
+
break
|
|
571
|
+
else:
|
|
572
|
+
raise ValueError(
|
|
573
|
+
f"Undefined convergence criterion: {self._convergence_criterion}"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
pbar1.close()
|
|
577
|
+
pbar2.close()
|
|
578
|
+
pbar3.close()
|
|
579
|
+
pbar4.close()
|
|
580
|
+
|
|
581
|
+
# get final result
|
|
582
|
+
final_Js = self._Js.copy()
|
|
583
|
+
final_phis = self._phis.copy()
|
|
584
|
+
revive_compartments_by_copy(final_Js, final_phis, self._kill_threshold, self._rng)
|
|
585
|
+
|
|
586
|
+
# store diagnostic output
|
|
587
|
+
self._diagnostics = {
|
|
588
|
+
"steps": steps,
|
|
589
|
+
"max_abs_incomp": max_abs_incomp,
|
|
590
|
+
"max_abs_omega_diff": max_abs_omega_diff,
|
|
591
|
+
"max_abs_js_diff": max_abs_Js_diff,
|
|
592
|
+
"revive_count_left": self._revive_count_left,
|
|
593
|
+
"phis": final_phis,
|
|
594
|
+
"Js": final_Js,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
phases_volumes, phases_compositions = get_clusters(final_Js, final_phis)
|
|
598
|
+
|
|
599
|
+
return phases_volumes, phases_compositions
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def find_coexisting_phases(
|
|
603
|
+
chis: np.ndarray,
|
|
604
|
+
phi_means: np.ndarray,
|
|
605
|
+
num_compartments: int,
|
|
606
|
+
**kwargs,
|
|
607
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
608
|
+
"""
|
|
609
|
+
The convenience wrapper for class :class:`CoexistingPhasesFinder`. This function will
|
|
610
|
+
create the class :class:`CoexistingPhasesFinder` internally, and then conduct the
|
|
611
|
+
random initialization, finally use self consistent iterations to find coexisting
|
|
612
|
+
phases. :paramref:`kwargs` is forwarded to :class:`CoexistingPhasesFinder`. See class
|
|
613
|
+
:class:`CoexistingPhasesFinder` for all the possible options.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
chis:
|
|
617
|
+
The interaction matrix. 2D array with size of :math:`N_\\mathrm{c} \\times
|
|
618
|
+
N_\\mathrm{c}`. This matrix should be the full :math:`\\chi_{ij}` matrix of
|
|
619
|
+
the system, including the solvent component. Note that the matrix must be
|
|
620
|
+
symmetric, which is not checked but should be guaranteed externally.
|
|
621
|
+
phi_means:
|
|
622
|
+
The average volume fractions :math:`\\bar{\\phi}_i` of all the components of
|
|
623
|
+
the system. 1D array with size of :math:`N_\\mathrm{c}`. Note that the volume
|
|
624
|
+
fraction of the solvent is included as well, therefore the sum of this array
|
|
625
|
+
must be unity, which is not checked by this function and should be guaranteed
|
|
626
|
+
externally.
|
|
627
|
+
num_compartments:
|
|
628
|
+
Number of compartments :math:`M` in the system.
|
|
629
|
+
kwargs:
|
|
630
|
+
See class :class:`CoexistingPhasesFinder` for all the possible options.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
[0]:
|
|
634
|
+
Volume fractions of each phase :math:`J_\\alpha`. 1D array with the size of
|
|
635
|
+
:math:`N_\\mathrm{p}`.
|
|
636
|
+
[1]:
|
|
637
|
+
Volume fractions of components in each phase :math:`\\phi_i^{(\\alpha)}`. 2D
|
|
638
|
+
array with the size of :math:`N_\\mathrm{p} \\times N_\\mathrm{c}`.
|
|
639
|
+
"""
|
|
640
|
+
finder = CoexistingPhasesFinder(chis, phi_means, num_compartments, **kwargs)
|
|
641
|
+
return finder.run()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yicheng Qiang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flory
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Python package for finding coexisting phases in multicomponent mixtures
|
|
5
|
+
Author-email: Yicheng Qiang <yicheng.qiang@ds.mpg.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: homepage, https://github.com/qiangyicheng/flory
|
|
8
|
+
Project-URL: repository, https://github.com/qiangyicheng/flory
|
|
9
|
+
Keywords: physics,phase-separation,free-energy
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numba >=0.57
|
|
23
|
+
Requires-Dist: numpy >=1.22
|
|
24
|
+
Requires-Dist: scipy >=1.7
|
|
25
|
+
Requires-Dist: tqdm >=4.60
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest >=6.2 ; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov >=4.0 ; extra == 'dev'
|
|
29
|
+
Requires-Dist: Sphinx >=4 ; extra == 'dev'
|
|
30
|
+
Requires-Dist: sphinx-autodoc-typehints >=2.2 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: sphinx-paramlinks >=0.6 ; extra == 'dev'
|
|
32
|
+
Requires-Dist: sphinx-rtd-theme >=2.0 ; extra == 'dev'
|
|
33
|
+
|
|
34
|
+
# flory
|
|
35
|
+
|
|
36
|
+
[](https://github.com/qiangyicheng/flory/actions/workflows/python-package.yml)
|
|
37
|
+
[](https://codecov.io/github/qiangyicheng/flory)
|
|
38
|
+
[](https://flory.readthedocs.io/en/latest/?badge=latest)
|
|
39
|
+
|
|
40
|
+
`flory` is a Python package for determining multiple coexisting phases in multicomponent mixtures.
|
|
41
|
+
Although finding coexisting phases of binary mixture is simple, finding multiple coexisting phases in many-component mixtures is challenging, mainly due to the coupling of lots of nolinear equations.
|
|
42
|
+
Currently `flory` supports finding coexisting phases of arbitrary multicomponent system with Flory-Huggins interactions in the canonical ensemble, whose average free energy density reads
|
|
43
|
+
|
|
44
|
+
$$\bar{f}({N_\mathrm{p}}, \{J_\alpha\}, \{\phi_i^{(\alpha)}\}) = \sum_{\alpha=1}^{{N_\mathrm{p}}} J_\alpha f(\{\phi_i^{(\alpha)}\}) \; ,$$
|
|
45
|
+
|
|
46
|
+
where $N_\mathrm{c}$ is the number of components, $N_\mathrm{p}$ is the number of phases, $J_\alpha$ is the volume fraction of the phase $\alpha$ and $\phi_i^{(\alpha)}$ is the volume fraction of the component $i$ in the phase $\alpha$.
|
|
47
|
+
Each phase is considered to be homogeneous with a energy density
|
|
48
|
+
|
|
49
|
+
$$f(\{\phi_i\}) = \frac{1}{2}\sum_{i,j=1}^{N_\mathrm{c}} \chi_{ij} \phi_i \phi_j + \sum_{i=1}^{N_\mathrm{c}} \frac{\phi_i}{l_i} \ln \phi_i \; ,$$
|
|
50
|
+
|
|
51
|
+
where $\chi_{ij}$ is the Flory-Huggins interaction between component $i$ and $j$, and $l_i$ is the relative molecule size of the component $i$.
|
|
52
|
+
With given interaction matrix $\chi_{ij}$, average volume fractions of all components across the system $\bar{\phi}_i$ and the relative molecule sizes $l_i$, `flory` provides tools to find the coexisting phases in equilibrium.
|
|
53
|
+
|
|
54
|
+
Installation
|
|
55
|
+
------------
|
|
56
|
+
|
|
57
|
+
`flory` is available on `pypi`, so you should be able to install it through `pip`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install flory
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Usage
|
|
64
|
+
-----
|
|
65
|
+
|
|
66
|
+
A simple example finds the coexisting phases of a binary mixture:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import flory
|
|
70
|
+
|
|
71
|
+
chis = [[0, 4.0], [4.0, 0]] # Set the \chi matrix
|
|
72
|
+
phi_means = [0.5, 0.5] # Set the average volume fractions
|
|
73
|
+
|
|
74
|
+
volumes, phis = flory.find_coexisting_phases(chis, phi_means, 16) # obtain the relative volumes and the compositions of the two coexisting phases
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
More information
|
|
79
|
+
----------------
|
|
80
|
+
* See examples in [examples folder](https://github.com/qiangyicheng/flory/tree/main/examples)
|
|
81
|
+
* [Full documentation on readthedocs](https://flory.readthedocs.io/)
|
|
82
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
flory/__init__.py,sha256=nJKzfDFUBaQ7iAIMYIzGmG46gK0_qlQjpe1Rr8Q3fIM,582
|
|
2
|
+
flory/_version.py,sha256=Pv9ouMrHW4YJ2RqzWPcojtYRoZtNSgzCzgB3B3Q2FMU,411
|
|
3
|
+
flory/mcmp.py,sha256=rolC1EIChl2BuNUN9Adw3L6pN2qpRIqujf_zo38FKvA,29453
|
|
4
|
+
flory/detail/mcmp_impl.py,sha256=Q-0plKjUwq5byFzyNCrRa8uts5XkSK2NEqMbIAYfh6k,23394
|
|
5
|
+
flory-0.0.0.dist-info/LICENSE,sha256=Avmhj-OrLS-F1uar0j_Z6LllTF0oRBAlAV_JpRlGD6w,1070
|
|
6
|
+
flory-0.0.0.dist-info/METADATA,sha256=XdkhR_kMtxUvH0iiDzIC5MvaB6QdbwVBFhA4Lf6fxZ0,3974
|
|
7
|
+
flory-0.0.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
|
8
|
+
flory-0.0.0.dist-info/top_level.txt,sha256=jtZpdTpD2w3GUbSbuIwh0-DTaWB2Jz_Nqd6up2DxXBU,6
|
|
9
|
+
flory-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flory
|