codex/atp-adp-phosphate-cycle.py

atp-adp-phosphate-cycle.py
"""Draw ATP/ADP component bookkeeping, not molecular structures.

Tested with Python 3.14.4 and the repository's Matplotlib/NumPy environment.
Write an opaque white same-basename PNG to cwd, as required by Makefile.
"""

from pathlib import Path

import matplotlib

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


BLUE = "#19678d"
ORANGE = "#ad5516"
INK = "#24333d"


def components(ax, x, y, count, label):
    """Boxes and circles count components; lines do not depict actual bonds."""
    ax.add_patch(FancyBboxPatch(
        (x, y - 0.27), 1.72, 0.54, boxstyle="round,pad=0.05",
        facecolor="#edf5f8", edgecolor=BLUE, linewidth=1.6,
    ))
    ax.text(x + 0.86, y, "Adenine + ribose", ha="center", va="center",
            fontsize=11, color=INK)
    ax.text(x + 0.86, y - 0.53, "adenosine", ha="center", color=BLUE,
            fontsize=11)
    previous = x + 1.77
    for i in range(count):
        center = x + 2.15 + 0.68 * i
        ax.plot([previous, center - 0.25], [y, y], color=INK, lw=1.5)
        ax.add_patch(Circle((center, y), 0.25, facecolor="#fff0df",
                            edgecolor=ORANGE, lw=1.6))
        ax.text(center, y, "P", ha="center", va="center", fontsize=12,
                color=ORANGE, weight="bold")
        ax.text(center, y - 0.53, ["α", "β", "γ"][i], ha="center",
                fontsize=12, color=ORANGE)
        previous = center + 0.25
    ax.text(x + 1.76, y + 0.63, label, ha="center", fontsize=15,
            color=BLUE, weight="bold")


def main():
    fig, ax = plt.subplots(figsize=(8.0, 5.3))
    fig.set_facecolor("white")
    ax.set_facecolor("white")
    ax.set(xlim=(0, 9.3), ylim=(0, 6.1), aspect="equal")
    ax.axis("off")
    fig.subplots_adjust(left=0.025, right=0.975, top=0.95, bottom=0.05)
    ax.text(4.65, 5.85, "ATP ⇄ ADP: a reusable material carrier",
            ha="center", fontsize=15, weight="bold", color=INK)
    components(ax, 0.25, 3.22, 3, "ATP: 3 phosphate groups")
    components(ax, 5.03, 3.22, 2, "ADP: 2 phosphate groups")
    ax.text(8.35, 3.22, "+", ha="center", va="center", fontsize=19,
            color=INK)
    ax.add_patch(Circle((8.91, 3.22), 0.25, facecolor="#fff0df",
                        edgecolor=ORANGE, lw=1.6))
    ax.text(8.91, 3.22, "Pᵢ", ha="center", va="center", fontsize=12,
            color=ORANGE)
    ax.text(8.91, 2.69, "free", ha="center", fontsize=10, color=ORANGE)
    ax.add_patch(FancyArrowPatch(
        (2.16, 4.24), (7.05, 4.24), connectionstyle="arc3,rad=-0.08",
        arrowstyle="-|>", mutation_scale=18, color=BLUE, linewidth=2,
    ))
    ax.text(4.65, 4.95, "Hydrolysis consumes H₂O", ha="center",
            fontsize=12, color=BLUE)
    ax.text(4.65, 4.58, "Net free-energy decrease under stated conditions",
            ha="center", fontsize=10.5, color=BLUE)
    ax.add_patch(FancyArrowPatch(
        (7.05, 2.26), (2.16, 2.26), connectionstyle="arc3,rad=-0.12",
        arrowstyle="-|>", mutation_scale=18, color=ORANGE, linewidth=2,
    ))
    ax.text(4.65, 1.68, "Regeneration needs energy input", ha="center",
            fontsize=12, color=ORANGE)
    ax.text(4.65, 1.31, "The reverse net reaction forms H₂O", ha="center",
            fontsize=10.5, color=ORANGE)
    ax.text(4.65, 0.62, "Phosphate count: 3 = 2 + 1", ha="center",
            fontsize=12, color=INK)
    ax.text(4.65, 0.19, "Component schematic: connecting oxygens, charges, and shapes omitted",
            ha="center", fontsize=9.5, color="#53616a")
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=140,
                facecolor="white", transparent=False)
    plt.close(fig)
    print("Phosphate bookkeeping: ATP 3 = ADP 2 + inorganic phosphate 1")


if __name__ == "__main__":
    main()