codex/polymer-blend-miscibility-landscape.py

polymer-blend-miscibility-landscape.py
"""Original symmetric lattice-model illustration; no measured polymer data.

Run from the mirrored media directory with Python 3.14, NumPy 2.3.5 and
Matplotlib 3.10.7. Both chains have 100 equal-volume segments.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


N_SEGMENTS = 100
OUTPUT = "polymer-blend-miscibility-landscape.png"


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


def derivative(phi, chi):
    return np.log(phi / (1 - phi)) / N_SEGMENTS + chi * (1 - 2 * phi)


def curvature(phi, chi):
    return 1 / (N_SEGMENTS * phi) + 1 / (N_SEGMENTS * (1 - phi)) - 2 * chi


def coexistence_left(chi):
    low, high = 1e-8, 0.25
    assert derivative(low, chi) < 0 < derivative(high, chi)
    for _ in range(80):
        mid = (low + high) / 2
        if derivative(mid, chi) < 0:
            low = mid
        else:
            high = mid
    return (low + high) / 2


def main():
    phi = np.linspace(0.0001, 0.9999, 1600)
    coexist = coexistence_left(0.03)
    coexist_other = 1 - coexist
    common_height = free_energy(coexist, 0.03)
    np.testing.assert_allclose(derivative(coexist, 0.03), 0, atol=1e-12)
    np.testing.assert_allclose(free_energy(coexist_other, 0.03), common_height, atol=1e-12)
    assert curvature(0.5, 0.01) > 0 > curvature(0.5, 0.03)
    assert np.min(free_energy(phi, 0.03)) >= common_height - 1e-8

    plt.rcParams.update({"font.size": 10, "axes.spines.top": False, "axes.spines.right": False})
    fig, ax = plt.subplots(figsize=(7.6, 4.8), dpi=100, facecolor="white")
    ax.plot(phi, 1000 * free_energy(phi, 0.01), color="#007c91", lw=2.2, label=r"$\chi=0.01$: one minimum")
    ax.plot(phi, 1000 * free_energy(phi, 0.03), color="#ba4d00", lw=2.2, label=r"$\chi=0.03$: two minima")
    ax.plot([coexist, coexist_other], [1000 * common_height] * 2, color="#555555", ls="--", lw=1.5, label="Two-phase common tangent")
    ax.scatter([coexist, coexist_other], [1000 * common_height] * 2, color="#ba4d00", zorder=4)
    ax.annotate(f"Coexisting compositions\n{coexist:.4f} and {coexist_other:.4f}", xy=(coexist_other, 1000 * common_height), xytext=(0.62, -2.6), arrowprops={"arrowstyle": "->", "color": "#555555"}, color="#555555", ha="center", fontsize=9)
    ax.set(xlim=(0, 1), ylim=(-5.1, 1.1), xlabel=r"First-polymer volume fraction $\phi$", ylabel=r"$10^3 f(\phi)$ (dimensionless)")
    ax.grid(alpha=0.18)
    ax.legend(loc="upper center", bbox_to_anchor=(0.5, 1.12), ncol=3, frameon=False, fontsize=8)
    ax.set_title("Polymer blend mixing: one minimum or phase coexistence", fontsize=12, pad=43)
    fig.text(0.5, 0.035, "Original equal-segment lattice model, N₁ = N₂ = 100. No specific polymer pair or data fitted.", ha="center", fontsize=9)
    fig.subplots_adjust(left=0.105, right=0.975, top=0.77, bottom=0.18)
    fig.savefig(Path.cwd() / OUTPUT, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()