codex/saccharomyces-fermentation-carbon-balance.py

saccharomyces-fermentation-carbon-balance.py
"""Original carbon bookkeeping and ideal conversion toy, not measured yields.

Run from _media: MPLCONFIGDIR=/tmp/codex-wiki-matplotlib python ../saccharomyces-fermentation-carbon-balance.py
Tested with Python 3.14.4, Matplotlib 3.10.7 and NumPy 2.3.5.
Basis: C6H12O6 -> 2 C2H5OH + 2 CO2, with no biomass or other products.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle, FancyBboxPatch


def carbon_group(ax, x, y, count, color):
    for i in range(count):
        ax.add_patch(Circle((x + i * 0.29, y), 0.115, facecolor=color, edgecolor="#172c42"))
        ax.text(x + i * 0.29, y, "C", color="white", ha="center", va="center", fontsize=9, weight="bold")


def main():
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11})
    fig = plt.figure(figsize=(9.5, 6.8), dpi=120, facecolor="white")
    fig.suptitle("S. cerevisiae: where does glucose carbon go?", fontsize=17, weight="bold", y=0.97)
    ax = fig.add_axes([0.04, 0.59, 0.92, 0.29])
    ax.set(xlim=(0, 10), ylim=(0, 3))
    ax.axis("off")
    ax.text(5, 2.85, "Ideal alcoholic fermentation — carbon atoms counted, intermediates omitted", ha="center", fontsize=11)
    ax.add_patch(FancyBboxPatch((0.15, 0.45), 2.25, 1.65, boxstyle="round,pad=0.02", facecolor="#eef4fa", edgecolor="#39516b"))
    ax.text(1.275, 1.78, "1 glucose", ha="center", weight="bold")
    carbon_group(ax, 0.55, 1.20, 6, "#386b9e")
    ax.text(1.275, 0.68, "6 carbon atoms", ha="center", fontsize=10)
    ax.annotate("", xy=(3.42, 1.29), xytext=(2.53, 1.29), arrowprops={"arrowstyle": "->", "lw": 1.7})
    ax.text(2.98, 1.72, "enzymes", ha="center", fontsize=10)
    for x, label, count, color in [(3.55, "2 ethanol", 2, "#207567"), (6.93, "2 carbon dioxide", 1, "#bb651d")]:
        ax.add_patch(FancyBboxPatch((x, 0.45), 2.87, 1.65, boxstyle="round,pad=0.02", facecolor="white", edgecolor=color))
        ax.text(x + 1.435, 1.78, label, ha="center", weight="bold")
        carbon_group(ax, x + 0.65, 1.20, count, color)
        carbon_group(ax, x + 1.87, 1.20, count, color)
        ax.text(x + 1.435, 0.68, f"{2 * count} carbon atoms", ha="center", fontsize=10)
    ax.text(6.56, 1.26, "+", fontsize=18, ha="center")
    plot = fig.add_axes([0.13, 0.16, 0.71, 0.38])
    extent = np.linspace(0, 10, 101)
    plot.plot(extent, 10 - extent, color="#386b9e", lw=2.4, label="Glucose remaining")
    plot.plot(extent, 2 * extent, color="#207567", lw=3.5, label="Ethanol formed")
    plot.plot(extent, 2 * extent, color="#bb651d", lw=2.1, ls="--", label="CO₂ formed (same amount)")
    plot.scatter([10], [20], color="#bb651d", zorder=4, s=40)
    plot.set(xlim=(0, 10), ylim=(0, 22), xlabel="Glucose converted (mmol); initial amount = 10 mmol",
             ylabel="Amount (mmol)", title="Toy calculation: full conversion gives 20 mmol of each product")
    plot.set_xticks([0, 2, 4, 6, 8, 10])
    plot.set_yticks([0, 5, 10, 15, 20])
    plot.grid(alpha=0.2)
    plot.spines[["top", "right"]].set_visible(False)
    plot.legend(loc="upper left", fontsize=9.5)
    fig.text(0.5, 0.035, "No time axis, growth, respiration, side products or gas losses: theoretical bookkeeping, not a culture measurement.",
             ha="center", fontsize=9.5, color="#526477")
    fig.savefig(Path.cwd() / "saccharomyces-fermentation-carbon-balance.png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()