codex/receptor-phosphorylation-assay-control-logic.py

receptor-phosphorylation-assay-control-logic.py
"""Draw assay inference and controls; write the matching PNG to the CWD.

Tested with Python 3.14.4, Matplotlib 3.10.7 and NumPy 2.3.5.
This is a reasoning diagram, not a blot image or a measured time course.
"""

from pathlib import Path

import matplotlib

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


def box(ax, x, y, width, height, text, color="#216a94", size=11):
    ax.add_patch(FancyBboxPatch((x, y), width, height, boxstyle="round,pad=0.10",
                               edgecolor=color, facecolor="white", lw=1.8))
    ax.text(x + width / 2, y + height / 2, text, ha="center", va="center", fontsize=size, color="#23313c")


def arrow(ax, start, end):
    ax.annotate("", end, start, arrowprops={"arrowstyle": "->", "lw": 1.7, "color": "#52616b"})


def main():
    fig, ax = plt.subplots(figsize=(10.4, 7.2))
    fig.patch.set_facecolor("white")
    fig.subplots_adjust(left=0.03, right=0.97, top=0.91, bottom=0.06)
    ax.set(xlim=(0, 12), ylim=(0, 10))
    ax.axis("off")
    fig.suptitle("A phosphorylation readout needs several independent controls", y=0.975, fontsize=15)
    box(ax, 0.2, 8.0, 3.2, 1.25, "Matched samples\nligand omitted / added")
    box(ax, 4.4, 8.0, 3.2, 1.25, "Stop reactions at sampling\npreserve phosphorylation")
    box(ax, 8.6, 8.0, 3.2, 1.25, "Separate / detect\nphospho + total receptor")
    arrow(ax, (3.5, 8.6), (4.25, 8.6))
    arrow(ax, (7.7, 8.6), (8.45, 8.6))
    controls = [
        (0.2, "Receptor identity", "Receptor loss + independent antibody\nIs this the intended protein?", "#216a94"),
        (4.4, "Phosphate-dependent epitope", "Phosphatase-treated sample\nwith retained total receptor", "#b34f18"),
        (8.6, "Detection / abundance", "Loading, transfer, background\nand usable response range", "#287c61"),
    ]
    for x, title, text, color in controls:
        ax.text(x + 1.6, 7.0, title, ha="center", fontsize=11, weight="bold", color=color)
        box(ax, x, 5.35, 3.2, 1.2, text, color, 10)
        arrow(ax, (x + 1.6, 5.15), (x + 1.6, 4.55))
    box(ax, 0.2, 3.25, 11.6, 1.1,
        "After appropriate controls: a ligand-associated change in detected receptor phosphorylation\nA phospho / total-receptor ratio can track relative modification within a linear response range.", size=11)
    arrow(ax, (6.0, 3.05), (6.0, 2.55))
    box(ax, 0.2, 0.6, 5.45, 1.65, "Mechanistic follow-up\nKinase perturbation, phosphate removal,\nand direct activity / recruitment tests", "#216a94", 11)
    box(ax, 6.35, 0.6, 5.45, 1.65, "Remaining ambiguity\nSignal depends on addition AND removal.\nPhosphorylation alone is not catalytic flux.", "#b34f18", 11)
    fig.text(0.5, 0.025, "Original control logic; no bands, measurements, experimental outcomes or timing are drawn.", ha="center", fontsize=10)
    output = Path.cwd() / (Path(__file__).stem + ".png")
    fig.savefig(output, dpi=120, facecolor="white", transparent=False)
    plt.close(fig)
    print(output.name)


if __name__ == "__main__":
    main()