codex/copolymer-conversion-feasibility.py

copolymer-conversion-feasibility.py
"""Closed-batch monomer bookkeeping, not fitted copolymerization kinetics.

Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Writes PNG into cwd.
"""
from pathlib import Path

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


def remaining_feed_bounds(conversion, initial_fraction=0.20):
    """Residual monomer-1 fraction, for 0 <= conversion < 1."""
    x = np.asarray(conversion)
    if np.any((x < 0) | (x >= 1)) or not 0 <= initial_fraction <= 1:
        raise ValueError("Use an admissible initial fraction and conversion below one.")
    lower = np.maximum(0, (initial_fraction - x) / (1 - x))
    upper = np.minimum(1, initial_fraction / (1 - x))
    return lower, upper


def main():
    x = np.linspace(0, 0.9999, 1000)
    lower, upper = remaining_feed_bounds(x)
    fig, ax = plt.subplots(figsize=(7.2, 4.8), dpi=100, facecolor="white")
    ax.set_facecolor("white")
    ax.fill_between(x, lower, upper, color="#a8cbdc", alpha=0.8,
                    label="Allowed by component balances")
    ax.plot(x, lower, color="#266585", lw=1.5)
    ax.plot(x, upper, color="#266585", lw=1.5)
    ax.axhline(0.20, color="#777777", ls=":", lw=1.2,
               label="Initial monomer-1 fraction: 0.20")
    ax.axvline(1, color="#777777", ls="--", lw=1.1)
    ax.scatter([0.5], [0.3], color="#367b37", s=42, zorder=4)
    ax.annotate("X = 0.50, f = 0.30\nCumulative fraction = 0.10", (0.5, 0.3),
                xytext=(0.55, 0.39), fontsize=9,
                arrowprops={"arrowstyle": "-", "color": "#367b37"})
    ax.scatter([0.1], [0.5], color="#b33f38", marker="x", s=55, zorder=4)
    ax.annotate("Impossible: more monomer 1 remains\nthan was initially present", (0.1, 0.5),
                xytext=(0.17, 0.66), fontsize=8.5,
                arrowprops={"arrowstyle": "-", "color": "#b33f38"})
    ax.set(xlim=(0, 1.03), ylim=(0, 1), xlabel="Total molar conversion X",
           ylabel="Monomer-1 fraction f in the remaining pool",
           title="Mass balance constrains the residual monomer mixture")
    ax.grid(alpha=0.17)
    ax.legend(loc="upper left", fontsize=8)
    fig.text(0.5, 0.015, "Original closed-batch model: initial fraction 0.20; no feed or side losses.\n"
             "At X = 1 no monomer remains, so its residual composition is undefined.",
             ha="center", fontsize=8)
    fig.tight_layout(rect=(0, 0.07, 1, 1))
    fig.savefig(Path("copolymer-conversion-feasibility.png"), facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()