Examples

Here we provide a few examples for using package.

Find coexisting phases once

Since we only need to find the coexisting phases once, we assign values to the \(\chi_{ij}\) matrix and the average volume fractions \(\bar{\phi}_i\) , and call the wrapper function find_coexisting_phases() directly. Here we consider a symmetric two-component system, with \(\chi=4\):

 1import flory
 2
 3chis = [[0, 4.0], [4.0, 0]]
 4phi_means = [0.5, 0.5]
 5
 6phases = flory.find_coexisting_phases(2, chis, phi_means)
 7
 8with open(__file__ + ".out", "w") as f:
 9    print("Volumes:", phases.volumes, file=f)
10    print("Compositions:", phases.fractions, file=f)

We obtain two symmetric phases:

1Volumes: [0.5 0.5]
2Compositions: [[0.97875201 0.02124799]
3 [0.02124799 0.97875201]]

By adding a sizes parameter, we can also investigate the coexisting phases where the components have different molecule sizes:

 1import flory
 2
 3chis = [[0, 4.0], [4.0, 0]]
 4phi_means = [0.5, 0.5]
 5sizes = [1.0, 2.0]
 6
 7phases = flory.find_coexisting_phases(2, chis, phi_means, sizes=sizes)
 8
 9with open(__file__ + ".out", "w") as f:
10    print("Volumes:", phases.volumes, file=f)
11    print("Compositions:", phases.fractions, file=f)

which gives two asymmetric phases:

1Volumes: [0.49421383 0.50578617]
2Compositions: [[9.99096418e-01 9.02812053e-04]
3 [1.23228537e-02 9.87677899e-01]]

Extension to more components is straight forward:

 1import flory
 2
 3chis = [[3.27, -0.34, 0], [-0.34, -3.96, 0], [0, 0, 0]]
 4phi_means = [0.16, 0.55, 0.29]
 5sizes = [2.0, 2.0, 1.0]
 6
 7phases = flory.find_coexisting_phases(3, chis, phi_means, sizes=sizes)
 8
 9with open(__file__ + ".out", "w") as f:
10    print("Volumes:", phases.volumes, file=f)
11    print("Compositions:", phases.fractions, file=f)

which gives two phases:

1Volumes: [0.63347743 0.36652257]
2Compositions: [[0.07578503 0.81378509 0.11042987]
3 [0.30555252 0.09408838 0.60035913]]

Construct a 2D phase diagram

When constructing a phase diagram, we usually need to find coexisting phases for multiple instances. To avoid the creation and the destruction of the internal data each time, we provide the class API CoexistingPhasesFinder. Using the class API usually involves three steps: creation of the finder instance, setting the system parameters and finding the coexisting states. When the system sizes such as number of components \(N_\mathrm{C}\) and the number of compartments \(N_\mathrm{M}\) do not change, the finder can be reused. Here we provide a simple example for generating a \((\phi, \chi)\) phase diagram for a simple binary mixture:

 1import matplotlib.pyplot as plt
 2import numpy as np
 3
 4import flory
 5
 6chi_start = 5.0
 7chi_end = 1.0
 8
 9num_comp = 2
10chis = [[0.0, 0.0], [0.0, 0.0]]
11phi_means = [0.5, 0.5]
12
13free_energy = flory.FloryHuggins(num_comp, chis)
14interaction = free_energy.interaction
15entropy = free_energy.entropy
16ensemble = flory.CanonicalEnsemble(num_comp, phi_means)
17finder = flory.CoexistingPhasesFinder(
18    interaction,
19    entropy,
20    ensemble,
21    progress=False,
22)
23
24
25line_chi = []
26line_l = []
27line_h = []
28for chi in np.arange(chi_start, chi_end, -0.1):  # scan chi from high value to low value
29    interaction.chis = np.array([[0, chi], [chi, 0]])  # set chi matrix of the finder
30    finder.set_interaction(interaction)
31    phases = finder.run().get_clusters()  # get coexisting phases
32    if phases.fractions.shape[0] == 1:  # stop scanning if no phase separation
33        break
34    phi_h = phases.fractions[
35        0, 0
36    ]  # extract the volume fraction of component 0 in phase 0
37    phi_l = phases.fractions[
38        1, 0
39    ]  # extract the volume fraction of component 0 in phase 1
40    line_chi.append(chi)
41    line_l.append(phi_l)
42    line_h.append(phi_h)
43
44plt.plot(line_l, line_chi, c="black")
45plt.plot(line_h, line_chi, c="black")
46plt.xlabel("$\\phi$")
47plt.ylabel("$\\chi$")
48plt.savefig(__file__ + ".jpg")

We obtain the phase diagram

phase diagram example

Check finite size effect

The minimization process in our algorithm DO NOT guarantee that the equilibrium state is always found. Due to the multistability of the multicomponent mixture, tt is possible that the algorithm find a local minimum. For example, the true equilibrium state can be a 4-phase coexisting state, while the algorithm may find a metastable 3-phase coexisting state. This issue can be resolved by launching more compartments than phases. Obviously, the maximum number of phases can be found is always not larger than the number of the compartments. By launching more compartments, it makes the algorithm much more likely to find the correct coexisting states. Here we refer to this as the finite size effect, see an example of \(N_\mathrm{C}=8\) below:

 1import matplotlib.pyplot as plt
 2import numpy as np
 3
 4import flory
 5
 6N_comp = 8
 7
 8# create a random chi matrix
 9chi_mean = 0
10chi_std = 8
11rng = np.random.default_rng(2333)
12chis = rng.normal(chi_mean, chi_std, (N_comp, N_comp))
13chis = 0.5 * (chis + chis.T)
14chis *= 1.0 - np.identity(N_comp)
15
16phi_means = np.full(N_comp, 1.0 / N_comp)  # set a symmetric composition
17
18line_N_compartment = []
19line_N_phase = []
20for N_compartment in range(4, 16, 2):  # use different compartment number
21    phases = flory.find_coexisting_phases(
22        N_comp, chis, phi_means, num_part=N_compartment, progress=True
23    )
24    line_N_compartment.append(N_compartment)
25    line_N_phase.append(phases.volumes.shape[0])
26
27plt.plot(line_N_compartment, line_N_phase, c="black")
28plt.xlabel("number of compartments")
29plt.ylabel("number of phases")
30plt.savefig(__file__ + ".jpg")

We obtain

phase diagram example

showing that \(N_\mathrm{M}=4\) underestimates the number of phases in the final coexisting state, while larger \(N_\mathrm{M}\) values give the correct result.

Construct a ternary phase diagram

Here we provide a simple example for generating a \((\phi_B, \phi_A)\) phase diagram for a simple ternary mixture with fixed interaction matrix. The example first finds a point in the phase diagram that leads to three-phase coexistence, which is a triangle in the phase diagram. Then starting from each edge of the triangle, we follow the direction to the unknown region in the phase diagram to complete all two-phase coexistence regions.

 1import matplotlib.pyplot as plt
 2import numpy as np
 3
 4import flory
 5
 6num_comp = 3
 7chis = [[0.0, 2.4, 3.2], [2.4, 0.0, 2.8], [3.2, 2.8, 0.0]]
 8# we guess this point is a three-phase coexistence point
 9phi_means = [0.33, 0.33, 0.34]
10
11free_energy = flory.FloryHuggins(num_comp, chis)
12interaction = free_energy.interaction
13entropy = free_energy.entropy
14ensemble = flory.CanonicalEnsemble(num_comp, phi_means)
15options = {
16    "num_part": 8,
17    "progress": False,
18    "max_steps": 10000000,  # disable progress bar, allow more steps
19}
20finder = flory.CoexistingPhasesFinder(interaction, entropy, ensemble, **options)
21
22# determine the three phase coexistence
23phases = finder.run().get_clusters()
24assert phases.volumes.shape[0] == 3
25p3_phis = phases.fractions
26p3_center = np.mean(p3_phis, axis=0)
27p3_edges = [p3_phis[[1, 2]], p3_phis[[0, 2]], p3_phis[[0, 1]]]
28
29
30# function that scan the 2-phase coexistence until the boundary is reached
31def find_p2_boundaries(
32    init_tie: np.ndarray,
33    start_point: np.ndarray,
34    finder: flory.CoexistingPhasesFinder,
35    scan_step: float = 0.02,
36    scan_step_min: float = 0.000001,
37):
38    internal_finder = flory.CoexistingPhasesFinder(
39        interaction, entropy, ensemble, **options
40    )
41
42    internal_finder.reinitialize_from_omegas(
43        finder.omegas.copy()
44    )  # use previous internal fields to accelerate
45
46    previous = start_point
47    current_tie = init_tie
48    step = scan_step
49
50    ties = [init_tie]
51    while True:
52        tie_center = current_tie.mean(axis=0)
53        tie_dir = current_tie[1] - current_tie[0]
54        tie_dir = tie_dir / np.linalg.norm(tie_dir)
55        next_dir = tie_center - previous
56        next_dir = next_dir - np.dot(next_dir, tie_dir) * tie_dir
57        next_dir = next_dir / np.linalg.norm(next_dir)
58        next_phis = tie_center + step * next_dir
59
60        ensemble.phi_means = np.array(next_phis)
61        internal_finder.set_ensemble(ensemble)
62        backup_omegas = internal_finder.omegas.copy()
63        phases = internal_finder.run().get_clusters()
64
65        if phases.volumes.shape[0] == 2 and np.all(phases.fractions > 0):
66            ties.append(phases.fractions)
67            previous = tie_center
68            current_tie = phases.fractions
69        else:
70            internal_finder.reinitialize_from_omegas(backup_omegas)
71            if step > scan_step_min:
72                step /= 2
73            else:
74                break
75
76    return np.array(ties)
77
78
79p2_ties = [find_p2_boundaries(edge, p3_center, finder) for edge in p3_edges]
80
81# plot the allowed region
82plt.plot([0, 1, 0, 0], [1, 0, 0, 1], c="black")
83
84for ties in p2_ties:
85    # plot the phase boundaries
86    plt.plot(ties[:, 0, 0], ties[:, 0, 1], c="brown")
87    plt.plot(ties[:, 1, 0], ties[:, 1, 1], c="brown")
88    for tie in ties:
89        # plot the tie lines
90        plt.plot(tie[:, 0], tie[:, 1], c="gray")
91
92# plot the 3-phase region
93plt.plot(p3_phis[[0, 1, 2, 0], 0], p3_phis[[0, 1, 2, 0], 1], c="blue")
94
95plt.xlabel("$\\phi_A$")
96plt.ylabel("$\\phi_B$")
97plt.savefig(__file__ + ".jpg")

We obtain the phase diagram

phase diagram example

Using constraints

In many systems such as mixtures containing ions or chemical reactions, there are additional constraints. flory provides convenient ways to consider these constrains. For example, in a system with 5 components, with first four components are charged, flory can find the coexisting phases that each phase is charge neutral, by applying a LinearLocalConstraint:

 1import numpy as np
 2
 3import flory
 4
 5num_comp = 5
 6chis = np.zeros((num_comp, num_comp))
 7chis[0][1] = -7.0
 8chis[1][0] = -7.0
 9phi_means = [0.10, 0.10, 0.09, 0.09, 0]
10phi_means[4] = 1.0 - np.sum(phi_means)
11sizes = [10.0, 10.0, 1.0, 1.0, 1.0]
12
13Cs = [0.9, -0.9, -1, 1, 0]
14Ts = 0
15
16fh = flory.FloryHuggins(num_comp, chis, sizes=sizes)
17ensemble = flory.CanonicalEnsemble(num_comp, phi_means)
18constraint = flory.LinearLocalConstraint(num_comp, Cs, Ts)
19
20finder = flory.CoexistingPhasesFinder(
21    fh.interaction,
22    fh.entropy,
23    ensemble,
24    [constraint],
25    max_steps=1000000,
26    progress=True,
27)
28phases = finder.run().get_clusters()
29
30with open(__file__ + ".out", "w") as f:
31    print("Volumes:", phases.volumes, file=f)
32    print("Compositions:", phases.fractions, file=f)

Using different ensemble

When considering an open system, the volume fractions are no longer conserved. Instead, the system will keep fixed chemical potentials. flory can handle this by switching from CanonicalEnsemble to GrandCanonicalEnsemble:

 1import numpy as np
 2
 3import flory
 4
 5num_comp = 3
 6chis = (1 - np.identity(num_comp)) * 5
 7
 8mus = [-1, 0.0, 0.1]
 9
10Cs = [[1, 1, 0]]
11Ts = [0.4]
12
13fh = flory.FloryHuggins(num_comp, chis)
14ensemble = flory.GrandCanonicalEnsemble.from_chemical_potential(num_comp, mus)
15constraint = flory.LinearGlobalConstraint(num_comp, Cs, Ts)
16
17finder = flory.CoexistingPhasesFinder(
18    fh.interaction,
19    fh.entropy,
20    ensemble,
21    constraint,
22    random_std=1.0,  # use less aggressive randomness to avoid rapid dying of compartments
23    progress=True,
24    tolerance=1e-12,
25)
26phases = finder.run().get_clusters()
27
28with open(__file__ + ".out", "w") as f:
29    print("Volumes:", phases.volumes, file=f)
30    print("Compositions:", phases.fractions, file=f)

Using different interaction&entropy

In a mixture with polydispersity, several components may share the same interaction property but only differs in size. flory allows to handle this case sufficiently by using different interaction and entropy:

 1import numpy as np
 2
 3import flory
 4
 5num_feat = 2
 6chis_feat = [[0, 4.0], [4.0, 0]]
 7phi_means = [0.2, 0.3, 0.2, 0.3]
 8sizes = [1, 2, 1, 2]
 9num_comp_per_feat = [2, 2]
10num_comp = np.sum(num_comp_per_feat)
11
12interaction = flory.FloryHugginsBlockInteraction(num_feat, chis_feat, num_comp_per_feat)
13entropy = flory.IdealGasPolydispersedEntropy(num_feat, sizes, num_comp_per_feat)
14ensemble = flory.CanonicalEnsemble(num_comp, phi_means)
15finder = flory.CoexistingPhasesFinder(interaction, entropy, ensemble)
16
17phases = finder.run().get_clusters()
18
19with open(__file__ + ".out", "w") as f:
20    print("Volumes:", phases.volumes, file=f)
21    print("Compositions:", phases.fractions, file=f)