codexpolymerization-monomers-conversion-models.py
polymerization-monomers-conversion-models.py
"""Toy analytic number-average chain counts, not measured distributions.
Tested with Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5.
Run from _media with MPLCONFIGDIR=/tmp/codex-wiki-matplotlib.
The matching PNG basename is written to the current working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
NAVY, TEAL, ORANGE = "#18364b", "#007f82", "#c75f18"
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11,
"text.color": NAVY, "axes.labelcolor": NAVY,
"axes.edgecolor": NAVY, "figure.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(11.2, 6.3), dpi=100)
fig.subplots_adjust(left=.085, right=.96, bottom=.31, top=.77, wspace=.30)
fig.suptitle("Conversion controls two different ideal chain counts", y=.965, fontsize=17, weight="bold")
fig.text(.5, .885, "TOY ANALYTIC MODELS • no experimental points or measured chain-length distributions", ha="center", fontsize=10)
ax = axes[0]
p = np.linspace(0, .999, 1600)
ax.semilogy(p, 1/(1-p), lw=2.6, color=TEAL, label="Balanced groups: r = 1")
ax.semilogy(p, 1.98/(1.98-1.96*p), lw=2.6, color=ORANGE, label="Group ratio: r = 0.98")
ax.scatter([1], [99], color=ORANGE, s=30, zorder=4)
ax.annotate("At p = 1: Xₙ = 99", xy=(1, 99), xytext=(.37, 165),
fontsize=10, arrowprops={"arrowstyle": "->", "color": ORANGE})
ax.set(xlim=(0, 1.01), ylim=(1, 1300), xlabel="p: fraction of limiting reactive groups consumed",
ylabel="Xₙ: monomeric units per chain (log scale)", title="Step growth: imbalance limits the mean")
ax.legend(loc="upper left", fontsize=10, frameon=False)
ax.set_yticks([1, 10, 100, 1000], ["1", "10", "100", "1000"])
ax.text(.03, .08, r"$X_n=(1+r)/(1+r-2rp)$", transform=ax.transAxes, fontsize=13)
ax = axes[1]
for ratio, color in [(200, TEAL), (100, ORANGE)]:
# A stipulated one-monomer initiation per chain sets the fixed-chain
# interval's lower endpoint: q = 1/R, X_n = 1.
q = np.linspace(1/ratio, 1, 501)
ax.plot(q, q*ratio, lw=2.6, color=color, label=f"R = {ratio} feed molecules / chain")
ax.scatter([.5], [100], color=TEAL, s=30, zorder=4)
ax.annotate("Half the feed; mean = 100", xy=(.5, 100), xytext=(.10, 146),
fontsize=10, arrowprops={"arrowstyle": "->", "color": TEAL})
ax.set(xlim=(0, 1), ylim=(0, 210), xlabel="q: fraction of feed monomer molecules incorporated",
ylabel="Xₙ: monomeric units per initiated chain", title="Ideal living growth: fixed chain count")
ax.legend(loc="lower right", fontsize=9.5, frameon=False)
ax.text(.04, .87, r"$X_n=qR,\quad R=M_0/C$", transform=ax.transAxes, fontsize=13)
for ax in axes:
ax.grid(alpha=.17)
ax.spines[["top", "right"]].set_visible(False)
ax.tick_params(colors=NAVY)
fig.text(.085, .135, "Linear chains, two groups per feed molecule;\nno rings, branches, or chain-material loss.\nResidual monomers count among chains.", fontsize=10)
fig.text(.575, .135, "One unit initiates each chain (curves start at Xₙ = 1);\nno termination, irreversible transfer, or new chains.\nThe mean does not imply equal chain lengths.", fontsize=10)
fig.text(.5, .037, "p and q count different entities. The straight line is not a prediction for every chain-growth process.",
ha="center", fontsize=10, color="#526773")
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
plt.close(fig)