codex/past-exam-of-the-quantum-condensed-matter-course-of-the-university-of-cambridge/2007-polymer-phase-diagram.py

past-exam-of-the-quantum-condensed-matter-course-of-the-university-of-cambridge/2007-polymer-phase-diagram.py
"""Original Flory-Huggins phase diagram; coexistence from a common tangent.

The inner bisections find the two stable stationary concentrations for a
trial chemical potential. The outer one equates their grand potentials.
Only NumPy and Matplotlib are required; output goes to the current directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

N = 1000
critical_phi = 1 / (1 + np.sqrt(N))
critical_chi = 0.5 * (1 + 1 / np.sqrt(N))**2


def free_energy(phi, chi):
    return phi / N * np.log(phi) + (1 - phi) * np.log1p(-phi) + chi * phi * (1 - phi)


def chemical_potential(phi, chi):
    return (np.log(phi) + 1) / N - np.log1p(-phi) - 1 + chi * (1 - 2 * phi)


def bisect(function, lower, upper, iterations=65):
    left_value = function(lower)
    right_value = function(upper)
    assert left_value * right_value <= 0, (lower, upper, left_value, right_value)
    for _ in range(iterations):
        middle = (lower + upper) / 2
        value = function(middle)
        if value * left_value > 0:
            lower, left_value = middle, value
        else:
            upper = middle
    return (lower + upper) / 2


def coexistence(chi):
    roots = np.sort(np.roots([2 * chi * N, N - 1 - 2 * chi * N, 1]))
    spin_low, spin_high = roots

    def phases(mu):
        low = np.exp(bisect(lambda u: chemical_potential(np.exp(u), chi) - mu,
                            -700, np.log(spin_low)))
        high = bisect(lambda p: chemical_potential(p, chi) - mu,
                      spin_high, 1 - 1e-12)
        return low, high

    def difference(mu):
        low, high = phases(mu)
        return free_energy(high, chi) - mu * high - free_energy(low, chi) + mu * low

    # Stay just inside the metastability limits to avoid roundoff at double roots.
    minimum = chemical_potential(spin_high, chi)
    maximum = chemical_potential(spin_low, chi)
    pad = (maximum - minimum) * 1e-9
    mu = bisect(difference, minimum + pad, maximum - pad)
    low, high = phases(mu)
    assert low < spin_low < spin_high < high
    assert abs(chemical_potential(low, chi) - chemical_potential(high, chi)) < 1e-10
    assert abs(difference(mu)) < 1e-10
    return low, high, spin_low, spin_high


chis = np.linspace(critical_chi + 0.00015, 0.61, 180)
branches = np.array([coexistence(chi) for chi in chis])
fig, ax = plt.subplots(figsize=(8.5, 5.2), layout="constrained")
ax.fill_betweenx(chis, branches[:, 0], branches[:, 1], color="#e6edf8",
                 label="two-phase equilibrium")
ax.fill_betweenx(chis, branches[:, 2], branches[:, 3], color="#f8d2c5",
                 label="unstable homogeneous state")
for column in [0, 1]:
    ax.plot(np.r_[critical_phi, branches[:, column]], np.r_[critical_chi, chis],
            color="#245b9f", lw=2, label="binodal" if column == 0 else None)
for column in [2, 3]:
    ax.plot(np.r_[critical_phi, branches[:, column]], np.r_[critical_chi, chis],
            color="#b55032", lw=1.6, ls="--", label="spinodal" if column == 2 else None)
ax.axhline(0.5, color="#447747", lw=1.3, ls=":", label=r"theta condition $\chi=1/2$")
ax.plot(critical_phi, critical_chi, "o", color="black", ms=5)
ax.annotate("critical point", (critical_phi, critical_chi), xytext=(0.002, 0.518),
            arrowprops={"arrowstyle": "->", "lw": 0.8}, fontsize=10)
ax.text(1.6e-10, 0.492, "good solvent", color="#447747", fontsize=10)
ax.text(1.6e-10, 0.505, "poorer solvent above the theta line", fontsize=10)
ax.text(0.012, 0.584, "unstable", color="#873e29", fontsize=10)
ax.text(5e-7, 0.574, "metastable", color="#245b9f", fontsize=10)
ax.set(xscale="log", xlim=(1e-10, 1), ylim=(0.48, 0.612),
       xlabel=r"Polymer volume fraction $\phi$ (log scale)",
       ylabel=r"Interaction parameter $\chi$ (larger = poorer solvent)",
       title=r"Flory-Huggins polymer solution, $N=1000$")
ax.legend(loc="upper left", fontsize=9, framealpha=0.96)
ax.grid(alpha=0.12)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120, facecolor="white")