codex/citric-acid-cycle-carbon-and-carriers.py

citric-acid-cycle-carbon-and-carriers.py
"""Draw one forward citric acid cycle; run from _media with Python 3.14.

Requires matplotlib 3.10.7 and numpy 2.3.5, as in pyproject.toml.
All positions are schematic. Carbon counts exclude the coenzyme A carrier.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig = plt.figure(figsize=(12, 8), facecolor="white")
    ax = fig.add_axes((0.07, 0.13, 0.62, 0.76))
    ax.set(xlim=(-1.65, 8.8), ylim=(-0.5, 8.6))
    ax.axis("off")
    nodes = [
        ("Oxaloacetate", 4, (0, 6.5)),
        ("Citrate", 6, (3.4, 6.5)),
        ("Isocitrate", 6, (6.8, 6.5)),
        (r"$\alpha$-Ketoglutarate", 5, (6.8, 3.7)),
        ("Succinyl-CoA", 4, (6.8, 0.9)),
        ("Succinate", 4, (3.4, 0.9)),
        ("Fumarate", 4, (0, 0.9)),
        ("Malate", 4, (0, 3.7)),
    ]
    for i, (_, _, start) in enumerate(nodes):
        end = nodes[(i + 1) % len(nodes)][2]
        direction = np.array(end) - start
        direction = direction / np.linalg.norm(direction)
        # Elliptical node boxes need more horizontal than vertical clearance.
        gap = 1.12 if abs(direction[0]) > 0 else 0.57
        ax.add_patch(FancyArrowPatch(
            np.array(start) + gap * direction,
            np.array(end) - gap * direction,
            arrowstyle="-|>", mutation_scale=18, lw=2, color="#536472",
        ))
    for name, carbon, (x, y) in nodes:
        ax.text(x, y, f"{name}\n{carbon} C", ha="center", va="center",
                bbox={"boxstyle": "round,pad=0.4", "fc": "#edf5fa", "ec": "#527f9c"})
    ax.annotate("", xy=(1.7, 6.5), xytext=(1.7, 7.85),
                arrowprops={"arrowstyle": "-|>", "lw": 2, "color": "#9c5d18"})
    ax.text(1.7, 8.12, "Acetyl-CoA\n2 C acetyl group", ha="center", color="#825017")
    ax.text(5.1, 5.88, "rearrangement", ha="center", fontsize=10)
    ax.text(7.08, 5.08, "NADH\n+ CO₂", va="center", color="#27644f")
    ax.text(7.08, 2.28, "NADH\n+ CO₂", va="center", color="#27644f")
    ax.text(5.1, 0.02, "GTP (or ATP)", ha="center", color="#825017")
    ax.text(1.7, 0.02, "QH₂ via bound FAD", ha="center", color="#27644f", fontsize=10)
    ax.text(-0.35, 2.3, "+ H₂O", ha="right", va="center", fontsize=11)
    ax.text(-0.35, 5.08, "NADH", ha="right", va="center", color="#27644f")
    ax.text(3.4, 3.8, "4 + 2 − 1 − 1 = 4\nacceptor regenerated",
            ha="center", va="center", fontsize=14, color="#38424b", linespacing=1.6)
    ledger = fig.add_axes((0.76, 0.2, 0.22, 0.62))
    ledger.axis("off")
    ledger.text(0, 1, "Per acetyl group", weight="bold", fontsize=15)
    for y, text in [(0.84, "3 NADH"), (0.71, "1 QH₂"), (0.58, "1 GTP (or ATP)"), (0.45, "2 CO₂")]:
        ledger.text(0, y, text, fontsize=15)
    ledger.text(0, 0.2, "QH₂ is the mobile output.\nOne FADH₂ equivalent\nis an alternative count,\nnot another output.",
                fontsize=11, linespacing=1.5)
    fig.suptitle("Citric acid cycle: carbon and carrier accounting", fontsize=19, y=0.97)
    fig.text(0.5, 0.035, "Original schematic • carbon skeletons, not molecular structures • no rates or ATP-conversion assumptions",
             ha="center", fontsize=11, color="#4d5960")
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()