codexantibody-equilibrium-occupancy-and-assay-readout.py
antibody-equilibrium-occupancy-and-assay-readout.py
"""Stipulated 1:1 occupancy model and assay interpretation, not measured data.
Python 3.14, matplotlib 3.10.7, numpy 2.3.5. Run from _media; writes to CWD.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
plt.rcParams.update({'font.size': 11, 'font.family': 'DejaVu Sans'})
fig, (curve, workflow) = plt.subplots(1, 2, figsize=(10.8, 5.4), facecolor='white',
gridspec_kw={'width_ratios': [1.05, 1]})
fig.subplots_adjust(left=.075, right=.975, bottom=.18, top=.88, wspace=.28)
c = np.geomspace(.5, 2000, 500)
for kd, color in ((20, '#2478a5'), (200, '#a53773')):
curve.semilogx(c, c/(kd+c), lw=2.5, color=color, label=rf'$K_d={kd}\,\mathrm{{nM}}$')
for value, label, offset in ((2, r'$1/11$', (8, 10)), (20, r'$1/2$', (-35, 8)),
(200, r'$10/11$', (6, -24))):
theta=value/(20+value)
curve.plot(value, theta, 'o', color='#2478a5', ms=6)
curve.annotate(label, (value, theta), xytext=offset, textcoords='offset points', color='#104762')
curve.axhline(.5, color='#aaaaaa', lw=.7, ls=':')
curve.set(xlim=(.5, 2000), ylim=(0, 1.04), xlabel=r'Free antigen concentration $c$ (nM)',
ylabel=r'Fraction of sites occupied $\theta$')
curve.set_xticks([1, 2, 20, 200, 2000], labels=['1', '2', '20', '200', '2000'])
curve.grid(alpha=.15)
curve.legend(loc='lower right', frameon=False)
curve.set_title(r'Stipulated equilibrium: $\theta=c/(K_d+c)$', fontsize=12, pad=16)
curve.spines[['top', 'right']].set_visible(False)
workflow.set(xlim=(0, 1), ylim=(0, 1)); workflow.axis('off')
workflow.set_title('What an assay adds to binding', fontsize=12, pad=16)
items=[(.88, 'Accessible epitopes', 'transfer, folding, surface presentation'),
(.66, 'Binding-site occupancy', 'affinity, concentration; possible avidity'),
(.44, 'Material retained after washing', 'detachment and rebinding'),
(.22, 'Recorded assay signal', 'label, background, detector saturation')]
for y, title, detail in items:
workflow.add_patch(FancyBboxPatch((.035,y-.075), .93, .15,
boxstyle='round,pad=.015', facecolor='#f4f7fa', edgecolor='#a0aab7'))
workflow.text(.5, y+.02, title, ha='center', va='center', weight='bold', fontsize=10.5)
workflow.text(.5, y-.035, detail, ha='center', va='center', fontsize=9.2, color='#444444')
for a,b in zip(items,items[1:]):
workflow.annotate('', (.5,b[0]+.09), (.5,a[0]-.09),
arrowprops={'arrowstyle': '->', 'color': '#666666'})
workflow.text(.5,.035, 'Half-maximal signal need not mean half occupancy.', ha='center', fontsize=10, color='#a53773')
fig.text(.5,.065, 'Original model and conceptual workflow • invented constants • no sensorgrams or experimental signals',
ha='center', fontsize=10, color='#555555')
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), dpi=110, facecolor='white', transparent=False)
plt.close(fig)