codexdna-antiparallel-base-pairs.py
dna-antiparallel-base-pairs.py
"""Draw canonical DNA pair counts; write dna-antiparallel-base-pairs.png in CWD.
Run from _media: python ../dna-antiparallel-base-pairs.py
Tested with Python 3.14.4, Matplotlib 3.10.7 and NumPy 2.3.5.
Original unrolled schematic: boxes are bases, not atomic structures.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
def main():
sequence = "ACGTAC"
partner = sequence.translate(str.maketrans("ACGT", "TGCA"))
counts = [2 if base in "AT" else 3 for base in sequence]
assert partner == "TGCATG" and partner[::-1] == "GTACGT"
assert counts == [2, 3, 3, 2, 2, 3] and sum(counts) == 15
colors = {"A": "#f4cb72", "T": "#a9dce8", "G": "#c8b9e7", "C": "#add8b0"}
ink = "#31485c"
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig, ax = plt.subplots(figsize=(8.8, 5.0))
fig.subplots_adjust(left=0.03, right=0.97, top=0.88, bottom=0.04)
ax.set(xlim=(-1.1, 6.0), ylim=(-0.9, 4.1))
ax.axis("off")
fig.suptitle("DNA: antiparallel strands and canonical base pairs", fontsize=16)
for y, left, right in [(3.0, "5′", "3′"), (0.5, "3′", "5′")]:
ax.plot([-0.35, 5.35], [y, y], color=ink, lw=3)
ax.text(-0.75, y, left, ha="center", va="center", fontsize=18)
ax.text(5.75, y, right, ha="center", va="center", fontsize=18)
for i in range(6):
ax.text(i, y, "S", ha="center", va="center", fontsize=11,
bbox={"boxstyle": "square,pad=0.22", "fc": "white", "ec": ink})
if i < 5:
ax.text(i + 0.5, y, "P", ha="center", va="center", fontsize=10,
bbox={"boxstyle": "circle,pad=0.18", "fc": "white", "ec": ink})
for i, (top, bottom, count) in enumerate(zip(sequence, partner, counts)):
ax.plot([i, i], [2.63, 3.0], color=ink, lw=2)
ax.plot([i, i], [0.5, 0.87], color=ink, lw=2)
for y, base in [(2.35, top), (1.15, bottom)]:
ax.add_patch(FancyBboxPatch((i - 0.28, y - 0.27), 0.56, 0.54,
boxstyle="round,pad=0.02", facecolor=colors[base],
edgecolor=ink, zorder=3))
ax.text(i, y, base, ha="center", va="center", fontsize=18, zorder=4)
for j in range(count):
x = i + (j - (count - 1) / 2) * 0.13
ax.plot([x, x], [1.46, 2.04], color=ink, lw=1.5, ls=":")
ax.text(i, 1.75, str(count), ha="center", va="center", fontsize=10,
bbox={"fc": "white", "ec": "none", "pad": 0.5}, zorder=5)
for start, end, y in [(-0.25, 5.25, 3.55), (5.25, -0.25, 0.05)]:
ax.annotate("", (end, y), (start, y),
arrowprops={"arrowstyle": "->", "lw": 1.8, "color": "#176b92"})
ax.text(2.5, 3.73, "Arrows show each strand’s 5′ → 3′ direction", ha="center", fontsize=11)
ax.text(2.5, -0.34, "3 A–T pairs × 2 + 3 G–C pairs × 3 = 15 hydrogen bonds",
ha="center", fontsize=12)
ax.text(2.5, -0.72, "S: sugar P: phosphate Dotted lines: hydrogen bonds\n"
"Unrolled schematic; connector positions do not show atomic geometry",
ha="center", va="center", fontsize=10, color=ink)
fig.savefig(Path(Path(__file__).stem + ".png"), dpi=120,
facecolor="white", transparent=False)
plt.close(fig)
print(f"5′–{sequence}–3′ / 3′–{partner}–5′; counts: {counts}; total: {sum(counts)}")
if __name__ == "__main__":
main()