codex/pcr-thermal-cycle-stage-chart.py

pcr-thermal-cycle-stage-chart.py
"""Draw an illustrative thermal schedule, not measured thermocycler data.

Tested with Python 3.14 / matplotlib 3.10.7; run from _media.
Writes pcr-thermal-cycle-stage-chart.png in the CWD.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Patch


def main():
    plt.rcParams.update({'font.size': 12, 'font.family': 'DejaVu Sans'})
    fig, ax = plt.subplots(figsize=(10, 5.5), dpi=120)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')
    holds = [(20, 94, '#b44435', 'Denaturation', 'separate strands'),
             (25, 55, '#2166ac', 'Annealing', 'pair primers'),
             (45, 72, '#16704a', 'Extension', 'copy from 3′ ends')]
    cursor = 0
    for cycle in range(3):
        previous = None
        for duration, temp, color, _, _ in holds:
            if previous is not None:
                ax.plot([cursor, cursor], [previous, temp], color='#7a818c', lw=1.3, ls='--')
            elif cycle:
                ax.plot([cursor, cursor], [72, temp], color='#7a818c', lw=1.3, ls='--')
            ax.axvspan(cursor, cursor + duration, color=color, alpha=0.08, lw=0)
            ax.plot([cursor, cursor + duration], [temp, temp], color=color, lw=4, solid_capstyle='butt')
            ax.text(cursor + duration/2, temp + 2.2, f'{duration} s', ha='center', va='bottom', fontsize=11, color=color)
            cursor += duration
            previous = temp
        ax.text(cycle * 90 + 45, 45.5, f'Cycle {cycle + 1}', ha='center', fontsize=12, weight='bold')
        if cycle < 2:
            ax.axvline(cursor, color='#c4c8cf', lw=1)
    ax.set(xlim=(0, 270), ylim=(43, 104), xlabel='Cumulative stipulated hold time (seconds)', ylabel='Temperature (°C)', xticks=range(0, 271, 30), yticks=[55, 72, 94])
    ax.spines[['top', 'right']].set_visible(False)
    ax.grid(axis='y', color='#e3e5e9', lw=0.7)
    ax.set_axisbelow(True)
    fig.suptitle('PCR thermal cycle: three temperatures, three jobs', fontsize=16, weight='bold', y=0.97)
    ax.set_title('Illustrative holds at 94, 55 and 72 °C; durations are assumptions', fontsize=12, pad=14)
    handles = [Patch(facecolor=color, label=f'{name}: {job}') for _, _, color, name, job in holds]
    fig.legend(handles=handles, loc='lower center', bbox_to_anchor=(0.5, 0.09), ncol=1, frameon=False, fontsize=11)
    fig.text(0.5, 0.025, 'Ramps, initial separation and final extension omitted • Vertical jumps are schematic • Not a measured trace', ha='center', fontsize=10, color='#344054')
    fig.subplots_adjust(left=0.1, right=0.97, top=0.81, bottom=0.34)
    fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()