codex/ionotropic-metabotropic-receptors-mechanisms.py

ionotropic-metabotropic-receptors-mechanisms.py
"""Original receptor mechanism cartoons; run from _media to write the PNG.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Shapes are conceptual, not molecular structures; arrows carry no timing data.
"""
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Rectangle


BLUE = "#176b9b"
ORANGE = "#ba5a19"
INK = "#213547"
GREEN = "#267659"


def signal(ax, start, end, rad=0):
    ax.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>", mutation_scale=13,
                               lw=1.7, color=ORANGE, linestyle="--",
                               connectionstyle=f"arc3,rad={rad}"))


def label_box(ax, x, y, label, color=GREEN, width=2.15):
    ax.add_patch(FancyBboxPatch((x-width/2, y-0.32), width, 0.64,
                               boxstyle="round,pad=0.06", facecolor="white",
                               edgecolor=color, linewidth=1.6))
    ax.text(x, y, label, ha="center", va="center", fontsize=10.5, color=INK)


def pore(ax, x, ion_y=4.93):
    for dx in (-0.38, 0.16):
        ax.add_patch(FancyBboxPatch((x+dx, 3.58), 0.22, 1.13,
                                   boxstyle="round,pad=0.025", facecolor="#d8eaf5",
                                   edgecolor=BLUE, linewidth=1.6))
    ax.add_patch(Circle((x, ion_y), 0.10, facecolor=BLUE))
    ax.text(x, ion_y+0.24, "+", ha="center", va="center", color=BLUE, fontsize=15)
    ax.add_patch(FancyArrowPatch((x, ion_y-0.15), (x, 3.42),
                               arrowstyle="-|>", mutation_scale=13, lw=2, color=BLUE))


def receptor(ax, x):
    ax.add_patch(FancyBboxPatch((x-0.24, 3.62), 0.48, 1.03,
                               boxstyle="round,pad=0.04", facecolor="#fff1e4",
                               edgecolor=ORANGE, linewidth=1.6))
    ax.add_patch(Circle((x, 5.22), 0.13, facecolor=ORANGE))
    signal(ax, (x, 5.02), (x, 4.72))


def main():
    fig, axes = plt.subplots(1, 3, figsize=(11.2, 7.0))
    fig.patch.set_facecolor("white")
    fig.subplots_adjust(left=0.03, right=0.98, top=0.82, bottom=0.14, wspace=0.12)
    fig.suptitle("Recognition can reach a pore through different routes", fontsize=17,
                 color=INK, y=0.97)
    fig.text(0.5, 0.91, "MECHANISM MODELS • conceptual geometry • no measured timing",
             ha="center", fontsize=11, color=INK)
    for ax in axes:
        ax.set(xlim=(0, 4), ylim=(0, 6))
        ax.axis("off")
        ax.add_patch(Rectangle((0.1, 3.88), 3.8, 0.45, facecolor="#eef0f2",
                               edgecolor="#afb7c0", lw=0.9))
        ax.text(0.14, 4.40, "outside", fontsize=9, color="#52606d")
        ax.text(0.14, 3.68, "inside", fontsize=9, color="#52606d")

    ax = axes[0]
    ax.set_title("Ionotropic\nreceptor and pore in one complex", fontsize=12, color=INK, pad=10)
    pore(ax, 2.58)
    ax.add_patch(Circle((1.16, 5.22), 0.13, facecolor=ORANGE))
    ax.text(1.16, 5.63, "ligand", ha="center", fontsize=10, color=ORANGE)
    signal(ax, (1.28, 5.08), (2.16, 4.48))
    # Bracket unites the binding function and pore; it is not a second relay.
    ax.plot([1.74, 1.74, 3.15, 3.15], [4.73, 3.35, 3.35, 4.73], color=BLUE, lw=1.2)
    ax.text(2.48, 3.02, "binding changes pore gating", ha="center", fontsize=10, color=INK)
    label_box(ax, 2.00, 1.94, "ionic current", color=BLUE, width=2.4)
    ax.text(2.0, 0.62, "No obligatory intracellular relay\nbetween binding and this pore", ha="center",
            fontsize=10.5, linespacing=1.5, color=INK)

    ax = axes[1]
    ax.set_title("Metabotropic: local route\nreceptor and pore are separate", fontsize=12, color=INK, pad=10)
    receptor(ax, 1.16)
    ax.text(1.16, 5.63, "ligand", ha="center", fontsize=10, color=ORANGE)
    pore(ax, 2.95)
    signal(ax, (1.16, 3.51), (1.16, 3.02))
    label_box(ax, 1.16, 2.65, "G protein", width=1.64)
    signal(ax, (2.01, 2.65), (2.73, 3.66), rad=0.18)
    ax.text(3.02, 2.33, "channel", ha="center", fontsize=10, color=BLUE)
    ax.text(2.0, 1.36, "A G-protein component\nregulates the separate pore", ha="center",
            fontsize=10.5, linespacing=1.5, color=INK)
    ax.text(2.0, 0.40, "No obligatory diffusible\nsecond messenger", ha="center",
            fontsize=10.5, linespacing=1.5, color=INK)

    ax = axes[2]
    ax.set_title("Metabotropic: messenger route\nexample using stored calcium", fontsize=12, color=INK, pad=10)
    receptor(ax, 1.04)
    ax.text(1.04, 5.63, "ligand", ha="center", fontsize=10, color=ORANGE)
    pore(ax, 3.10)
    signal(ax, (1.04, 3.51), (1.04, 3.18))
    label_box(ax, 1.04, 2.83, "G protein", width=1.6)
    signal(ax, (1.04, 2.45), (1.04, 2.22))
    label_box(ax, 1.04, 1.85, "enzyme", width=1.6)
    signal(ax, (1.04, 1.46), (1.04, 1.23))
    label_box(ax, 1.04, 0.86, r"$\mathrm{IP}_3$", width=1.6)
    signal(ax, (1.91, 0.86), (2.32, 0.86))
    label_box(ax, 3.04, 0.86, "stored Ca²⁺\nreleased", width=1.28)
    signal(ax, (3.04, 1.23), (3.04, 3.46))
    ax.text(3.63, 2.36, "regulates\nchannel", ha="center", fontsize=9, color=ORANGE)

    fig.text(0.5, 0.070, "Blue solid arrows: possible net positive-ion entry. Orange dashed arrows: signal relay.",
             ha="center", fontsize=10.5, color=INK)
    fig.text(0.5, 0.027, "Ion flow still needs an electrochemical drive; arrows do not imply universal excitation.",
             ha="center", fontsize=10.5, color=INK)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=110, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()