codex/optical-tweezers-force-readout.py

optical-tweezers-force-readout.py
"""Original functional schematic; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media: python ../optical-tweezers-force-readout.py
The readout is a proposed calibration arrangement, not a historical reconstruction.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle, FancyBboxPatch


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, axes = plt.subplots(1, 2, figsize=(11.8, 6.0), facecolor="white")
    ax, flow = axes
    blue, red, green = "#2367a2", "#c04738", "#187b58"
    ax.set(xlim=(-2.8, 2.8), ylim=(-2.6, 3.3), aspect="equal")
    ax.axis("off")
    ax.set_title("A. Light changes momentum; the bead recoils", loc="left", pad=15)
    ax.axvline(0, color="0.75", ls="--", lw=1)
    ax.plot(0, 0.75, marker="+", ms=14, color="0.2")
    ax.text(0.12, 0.86, "beam focus", fontsize=10)
    ax.add_patch(Circle((0, -0.25), 0.85, fc="#e3f1f8", ec=blue, lw=2))
    # Trace a representative ray through a lossless sphere using Snell's law.
    # Lengths are arbitrary; the paraxial incident focus lies above the bead.
    center, radius = np.array([0.0, -0.25]), 0.85

    def hit(point, direction):
        offset = point - center
        b = offset @ direction
        disc = b * b - (offset @ offset - radius * radius)
        roots = [-b - np.sqrt(disc), -b + np.sqrt(disc)]
        return point + min(t for t in roots if t > 1e-8) * direction

    def refract(direction, normal, ratio):
        cosine = -normal @ direction
        return ratio * direction + (ratio * cosine - np.sqrt(1 - ratio ** 2 * (1 - cosine ** 2))) * normal

    incoming = np.array([-2.0, 2.9])
    direction = np.array([0.0, 0.75]) - incoming
    direction /= np.linalg.norm(direction)
    entry = hit(incoming, direction)
    inside = refract(direction, (entry - center) / radius, 1.33 / 1.60)
    leaving = hit(entry, inside)
    outgoing = refract(inside, -(leaving - center) / radius, 1.60 / 1.33)
    assert outgoing[1] < direction[1]  # More downward outgoing momentum: upward recoil.
    end = leaving + ((-2.05 - leaving[1]) / outgoing[1]) * outgoing
    for sign in (-1, 1):
        reflect = np.array([sign, 1])
        points = [reflect * p for p in (incoming, entry, leaving, end)]
        ax.plot(*zip(*points), color=blue, lw=2)
        for start, stop in [(points[0], points[0] + 0.5 * (points[1] - points[0])),
                            (points[2], points[3])]:
            ax.annotate("", stop, start, arrowprops={"arrowstyle": "->", "color": blue, "lw": 2})
    ax.text(-2.55, 2.9, "incoming\nlight", color=blue, va="top")
    ax.text(0, -1.7, "outgoing light\n(more axial)", color=blue, fontsize=10, ha="center")
    ax.annotate("", (0, 0.57), (0, -0.18), arrowprops={"arrowstyle": "->", "lw": 3, "color": green})
    ax.text(1.25, 0.3, "backward\ngradient force", color=green, va="center")
    ax.annotate("", (0, -1.4), (0, -0.88), arrowprops={"arrowstyle": "->", "lw": 3, "color": red})
    ax.text(1.3, -1.1, "forward\nscattering force", color=red, va="center")
    ax.text(0, -2.52, "Ray picture: bead diameter ≫ wavelength\nNet force vanishes at a stable equilibrium", ha="center", fontsize=10)

    flow.set(xlim=(0, 10), ylim=(0, 10))
    flow.axis("off")
    flow.set_title("B. Hold, read position, then calibrate force", loc="left", pad=15)

    def box(x, y, w, h, label, color="0.25"):
        flow.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.12", fc="white", ec=color, lw=1.6))
        flow.text(x + w / 2, y + h / 2, label, ha="center", va="center", fontsize=10, color=color)

    def arrow(start, end, color=blue):
        flow.annotate("", end, start, arrowprops={"arrowstyle": "->", "lw": 1.8, "color": color})

    box(1.5, 8.55, 7, 1.1, "Laser → beam expansion / steering")
    box(1.5, 6.8, 7, 1.05, "High-NA objective → bead in liquid")
    arrow((5, 8.4), (5, 8.0))
    box(0.35, 4.6, 4.0, 1.25, "Collected light\n→ quadrant photodiode")
    box(5.65, 4.6, 4.0, 1.25, "Image → camera\ncheck bead / wall height")
    arrow((3.1, 6.65), (2.35, 6.0))
    arrow((6.9, 6.65), (7.65, 6.0))
    box(0.35, 2.4, 4.0, 1.25, "Voltage V − V₀\nposition x = β(V − V₀)", green)
    box(5.65, 2.4, 4.0, 1.25, "Stage: fluid speed u\ndrag ζu = k⟨x⟩", green)
    arrow((2.35, 4.45), (2.35, 3.8), green)
    arrow((7.65, 4.45), (7.65, 3.8), green)
    box(1.5, 0.45, 7, 1.05, "Thermal spectrum / variance + drag check\nexternal force F = k⟨x⟩", red)
    arrow((2.35, 2.25), (3.1, 1.65), red)
    arrow((7.65, 2.25), (6.9, 1.65), red)
    fig.suptitle("Optical tweezers: forces and a proposed calibrated readout", fontsize=16, y=0.99)
    fig.text(0.5, 0.015, "Original functional diagram; paths and sizes are schematic. Panel B is not the 1986 apparatus.", ha="center", fontsize=10)
    fig.subplots_adjust(left=0.035, right=0.985, top=0.87, bottom=0.09, wspace=0.16)
    fig.savefig(Path(__file__).stem + ".png", dpi=110, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()