codexsaccharomyces-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()