codex/pcr-opposed-primers-strand-cycles.py

pcr-opposed-primers-strand-cycles.py
"""Original PCR strand schematic. Tested with Python 3.14 / matplotlib 3.10.7.
Run from _media; saves a matching opaque white PNG in the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch

OLD = '#55565b'
NEW = '#22835c'
FWD = '#1769aa'
REV = '#b33c76'


def strand(ax, left, right, y, direction, color, labels=True, lw=2.7):
    start, end = (left, right) if direction == 1 else (right, left)
    ax.add_patch(FancyArrowPatch((start, y), (end, y), arrowstyle='-|>',
                                mutation_scale=12, lw=lw, color=color))
    if labels:
        ax.text(left - .13, y, "5′" if direction == 1 else "3′",
                ha='right', va='center', fontsize=11, color=color)
        ax.text(right + .13, y, "3′" if direction == 1 else "5′",
                ha='left', va='center', fontsize=11, color=color)


def setup(ax, title):
    ax.set_xlim(-.9, 11)
    ax.set_ylim(-.1, 3.6)
    ax.axis('off')
    ax.set_title(title, fontsize=13, loc='left', fontweight='bold', pad=8)
    for x in (2, 8):
        ax.plot([x, x], [.45, 3], '--', color='#d0d3d5', lw=1, zorder=0)


def main():
    plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 11})
    fig, axes = plt.subplots(2, 3, figsize=(11, 7.9), dpi=120, facecolor='white')
    fig.subplots_adjust(left=.065, right=.96, top=.87, bottom=.12, wspace=.36, hspace=.62)
    fig.suptitle('Opposed primers select a bounded DNA interval', fontsize=17, y=.97)
    fig.text(.5, .917, 'Arrows mark 5′ → 3′ strand direction; dashed lines mark the eventual outer boundaries.', ha='center', fontsize=11)
    for ax, title in zip(axes[0], ['1  Separate strands', '2  Bind opposed primers', '3  Extend paired 3′ ends']):
        setup(ax, title)
        strand(ax, 0, 10, 2.6, 1, OLD)
        strand(ax, 0, 10, .85, -1, OLD)
    axes[0, 0].text(5, 1.7, 'Heat separates pairing\nBackbones remain intact', ha='center', va='center', fontsize=10)
    strand(axes[0, 1], 7, 8, 2.22, -1, REV)
    strand(axes[0, 1], 2, 3, 1.23, 1, FWD)
    # Once extended, a primer's former 3′ end is an internal position.
    strand(axes[0, 2], 7, 8, 2.22, -1, REV, labels=False)
    strand(axes[0, 2], 2, 3, 1.23, 1, FWD, labels=False)
    for x, y, label, color in [(8.15, 2.22, "5′", REV), (-.45, 2.22, "3′", NEW),
                                (1.55, 1.23, "5′", FWD), (10.45, 1.23, "3′", NEW)]:
        axes[0, 2].text(x, y, label, color=color, ha='center', va='center')
    axes[0, 1].text(5, 1.72, 'Extendible 3′ ends\nface into the interval', ha='center', va='center', fontsize=10)
    strand(axes[0, 2], 0, 7, 2.22, -1, NEW, labels=False)
    strand(axes[0, 2], 3, 10, 1.23, 1, NEW, labels=False)
    axes[0, 2].text(5, .23, 'First copies can extend beyond\nthe opposite primer site.', ha='center', fontsize=10)
    titles = ['Cycle 1: one bounded end', 'Cycle 2: first bounded strand', 'Cycle 3: bounded duplex']
    for ax, title in zip(axes[1], titles):
        setup(ax, title)
    strand(axes[1, 0], 0, 10, 2.6, 1, OLD)
    strand(axes[1, 0], 0, 7, 2.12, -1, NEW, labels=False)
    strand(axes[1, 0], 7, 8, 2.12, -1, REV, labels=False)
    axes[1, 0].text(-.45, 2.12, '3′', color=NEW, ha='center', va='center')
    axes[1, 0].text(8.45, 2.12, '5′', color=REV, ha='center', va='center')
    axes[1, 0].text(5, .7, 'Selected reverse-primer copy\nretains a long left flank.', ha='center', fontsize=10)
    strand(axes[1, 1], 0, 8, 2.12, -1, OLD)
    strand(axes[1, 1], 2, 3, 2.6, 1, FWD, labels=False)
    axes[1, 1].text(1.55, 2.6, '5′', color=FWD, ha='center', va='center')
    axes[1, 1].text(8.45, 2.6, '3′', color=NEW, ha='center', va='center')
    strand(axes[1, 1], 3, 8, 2.6, 1, NEW, labels=False)
    axes[1, 1].text(5, .7, 'Forward-primer copy stops\nat this template’s right end.', ha='center', fontsize=10)
    strand(axes[1, 2], 2, 8, 2.6, 1, OLD)
    strand(axes[1, 2], 2, 7, 2.12, -1, NEW, labels=False)
    strand(axes[1, 2], 7, 8, 2.12, -1, REV, labels=False)
    axes[1, 2].text(1.55, 2.12, '3′', color=NEW, ha='center', va='center')
    axes[1, 2].text(8.45, 2.12, '5′', color=REV, ha='center', va='center')
    axes[1, 2].text(5, .7, 'Both strands span only\nthe primer-defined interval.', ha='center', fontsize=10)
    fig.text(.5, .065, 'Gray: template in the displayed step   Green: new extension   Blue: forward primer   Pink: reverse primer', ha='center', fontsize=10)
    fig.text(.5, .025, 'Bottom row follows one lineage only; it omits other products. Original schematic, not an observed experiment.', ha='center', fontsize=10)
    fig.savefig(Path(__file__).stem + '.png', facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()