codex/soap-head-tail-grease-water.py

soap-head-tail-grease-water.py
"""Draw an original soap orientation schematic; Python 3.14, matplotlib 3.10."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyBboxPatch


HEAD = '#2377a5'
TAIL = '#bf6b20'


def main():
    plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 12})
    fig, ax = plt.subplots(figsize=(10, 5.4), dpi=100, facecolor='white')
    fig.subplots_adjust(left=0.02, right=0.98, top=0.97, bottom=0.04)
    ax.set(xlim=(0, 10), ylim=(0, 5))
    ax.set_aspect('equal')
    ax.axis('off')
    ax.text(5, 4.80, 'Soap: two regions, two different surroundings',
            ha='center', fontsize=19, weight='bold')
    ax.text(2.30, 4.17, 'A long-chain fatty carboxylate', ha='center', weight='bold')
    xs = np.linspace(0.55, 2.90, 13)
    ys = 2.90 + 0.09 * (-1.0) ** np.arange(13)
    ax.plot(xs, ys, color=TAIL, linewidth=5, solid_capstyle='round')
    ax.add_patch(Circle((3.22, 2.9), 0.31, facecolor=HEAD, edgecolor='white', linewidth=2))
    ax.text(3.22, 2.9, '−', color='white', ha='center', va='center', fontsize=20)
    ax.annotate('Hydrocarbon tail\nhydrophobic', xy=(1.50, 2.85), xytext=(1.50, 1.83),
                ha='center', color=TAIL, arrowprops={'arrowstyle': '->', 'color': TAIL})
    ax.annotate('Carboxylate head\nhydrophilic', xy=(3.22, 3.18), xytext=(3.00, 3.65),
                ha='center', color=HEAD, arrowprops={'arrowstyle': '->', 'color': HEAD})
    ax.text(2.25, 0.95, 'Both regions belong to the same ion:\nan amphiphile and a surfactant.',
            ha='center', linespacing=1.6)
    ax.add_patch(FancyBboxPatch((4.72, 0.66), 4.90, 3.65,
                 boxstyle='round,pad=0.10', facecolor='#f3f9fd', edgecolor='#c3dce9'))
    ax.text(7.16, 4.02, 'Water surrounds a coated grease droplet', ha='center', weight='bold', fontsize=12)
    center = np.array([7.16, 2.34])
    ax.add_patch(Circle(center, 0.88, facecolor='#f6dba6', edgecolor='#d4a04a', linewidth=1.5))
    for theta in np.linspace(0, 2 * np.pi, 18, endpoint=False):
        radial = np.array([np.cos(theta), np.sin(theta)])
        tangent = np.array([-np.sin(theta), np.cos(theta)])
        steps = np.linspace(0.47, 1.13, 8)
        wiggle = 0.033 * (-1.0) ** np.arange(8)
        xy = center + steps[:, None] * radial + wiggle[:, None] * tangent
        ax.plot(xy[:, 0], xy[:, 1], color=TAIL, linewidth=2.6, solid_capstyle='round')
        ax.add_patch(Circle(center + 1.20 * radial, 0.095, facecolor=HEAD, edgecolor='white', linewidth=0.6))
    ax.text(*center, 'Grease', ha='center', va='center', fontsize=14, weight='bold',
            bbox={'facecolor': '#f6dba6', 'edgecolor': 'none', 'pad': 3})
    ax.text(7.16, 0.84, 'Heads face water; tails enter grease.', ha='center', fontsize=12)
    ax.text(5, 0.18, 'Schematic cross-section: orientation only, not molecular sizes or measured structure.',
            ha='center', fontsize=11, color='#53616a')
    fig.savefig(Path(__file__).stem + '.png', dpi=100, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()