codex/centrifuge-rotor-and-separated-fractions.py

centrifuge-rotor-and-separated-fractions.py
"""Conceptual rotor geometry and pelleting; no measured particle counts.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; writes the matching PNG to the current directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyBboxPatch, Rectangle


def tube(ax, x, y, width, height, *, separated=False):
    shell = FancyBboxPatch(
        (x, y), width, height, boxstyle="round,pad=0,rounding_size=0.18",
        facecolor="#eaf4fc", edgecolor="#334155", linewidth=2,
    )
    ax.add_patch(shell)
    if separated:
        pellet = Rectangle((x + width - 0.28, y), 0.28, height,
                           facecolor="#be4933", edgecolor="none")
        pellet.set_clip_path(shell)
        ax.add_patch(pellet)
        points = [(0.45, 0.32), (0.85, 0.62), (1.2, 0.42), (width - 0.14, 0.5)]
        for px, py in points:
            ax.add_patch(Circle((x + px, y + py * height), 0.045, color="#2563a6"))
    else:
        for px, py in [(0.3, 0.3), (0.6, 0.65), (1.05, 0.3), (1.4, 0.65),
                       (1.8, 0.3), (2.1, 0.65)]:
            ax.add_patch(Circle((x + px, y + py * height), 0.09, color="#be4933"))
        for px, py in [(0.45, 0.72), (0.85, 0.3), (1.2, 0.65), (1.9, 0.65)]:
            ax.add_patch(Circle((x + px, y + py * height), 0.045, color="#2563a6"))


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, (left, right) = plt.subplots(1, 2, figsize=(10, 4.5), dpi=120)
    fig.patch.set_facecolor("white")
    for ax in (left, right):
        ax.set(xlim=(-0.3, 5.8), ylim=(-0.65, 4.4), aspect="equal")
        ax.axis("off")
    left.set_title("A  Rotor geometry during rotation", loc="left", weight="bold")
    left.add_patch(Circle((0.65, 2.35), 0.38, facecolor="#e2e8f0", edgecolor="#334155"))
    left.plot([0.65, 0.65], [0.65, 3.4], linestyle="--", color="#64748b")
    left.text(0.65, 3.6, "rotation axis", ha="center")
    left.plot([1.03, 2.1], [2.35, 2.35], color="#94a3b8", linewidth=9, zorder=0)
    tube(left, 2.1, 1.9, 2.6, 0.9, separated=True)
    left.annotate("", (4.9, 3.2), (2.05, 3.2),
                  arrowprops={"arrowstyle": "->", "color": "#be4933", "lw": 2})
    left.text(3.5, 3.42, "outward centrifugal acceleration", ha="center", fontsize=10)
    for y, endpoint, label in [(1.55, 2.1, r"$r_{\min}$"), (0.8, 4.7, r"$r_{\max}$")]:
        left.annotate("", (endpoint, y), (0.65, y),
                      arrowprops={"arrowstyle": "<->", "color": "#334155"})
        left.text((0.65 + endpoint) / 2, y - 0.3, label, ha="center")
    left.text(2.7, -0.4, "One swung-out tube shown; geometry is schematic", ha="center", fontsize=9)
    right.set_title("B  Pelleting changes the collected fractions", loc="left", weight="bold")
    tube(right, 0.5, 2.8, 2.6, 0.9)
    right.text(3.4, 3.25, "starting\nsuspension", va="center")
    right.annotate("", (1.8, 1.8), (1.8, 2.6), arrowprops={"arrowstyle": "->", "lw": 2})
    tube(right, 0.5, 0.7, 2.6, 0.9, separated=True)
    right.annotate("supernatant\nretains slower particles", (1.35, 1.15), (3.4, 1.8),
                   fontsize=10, arrowprops={"arrowstyle": "->", "color": "#334155"})
    right.annotate("pellet\nmay trap other particles", (2.98, 1.1), (3.4, 0.55),
                   fontsize=10, arrowprops={"arrowstyle": "->", "color": "#334155"})
    right.text(2.6, -0.4, "Colors and particle counts are illustrative, not measured", ha="center", fontsize=9)
    fig.subplots_adjust(left=0.03, right=0.98, bottom=0.05, top=0.9, wspace=0.16)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()