codex/hahn-echo-phase-refocusing.py

hahn-echo-phase-refocusing.py
"""Original ideal Hahn echo phases; Python 3.14, matplotlib 3.10, NumPy 2.3.

Run from _media; writes hahn-echo-phase-refocusing.png to the working directory.
Positive gamma: free rotation R_z(-Delta*t), preparation R_u(-pi/2),
refocusing R_v(-pi). No relaxation. Vectors are magnetization projections.
"""

from pathlib import Path

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


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig = plt.figure(figsize=(10.4, 6.4))
    grid = fig.add_gridspec(2, 4, height_ratios=[1, 1.15])
    offsets = np.array([-1.7, -0.85, 0.0, 0.85, 1.7])  # Delta*tau, radians
    colors = ["#3455a4", "#218c9b", "#333333", "#d18b19", "#bf3f59"]
    stages = [(0, False, r"Prepared: $t=0^+$"),
              (1, False, r"Spread: $t=\tau^-$"),
              (1, True, r"Reflected: $t=\tau^+$"),
              (2, True, r"Refocused: $t=2\tau$")]
    for k, (time, after, title) in enumerate(stages):
        ax = fig.add_subplot(grid[0, k])
        ax.add_patch(Circle((0, 0), 1, fill=False, color="#b5bec8"))
        ax.axhline(0, color="#dce0e5", lw=0.8)
        ax.axvline(0, color="#dce0e5", lw=0.8)
        phases = -offsets * (time - 2 if after else time)
        # At preparation/refocusing all arrows coincide; separate lengths expose colors.
        lengths = np.linspace(0.64, 1, 5) if time in (0, 2) else np.ones(5)
        for phase, color, length in zip(phases, colors, lengths):
            vector = length * np.array([-np.sin(phase), np.cos(phase)])
            ax.annotate("", vector, (0, 0),
                        arrowprops={"arrowstyle": "-|>", "color": color, "lw": 2})
        ax.text(1.08, -0.06, "$u$")
        ax.text(0.04, 1.09, "$v$")
        ax.set(xlim=(-1.22, 1.25), ylim=(-1.15, 1.3), aspect="equal", title=title)
        ax.axis("off")
    ax = fig.add_subplot(grid[1, :])
    for delta, color in zip(offsets, colors):
        left = np.linspace(0, 1, 100)
        right = np.linspace(1, 2, 100)
        ax.plot(left, -delta * left, color=color, lw=2,
                label=rf"$\Delta\tau={delta:g}$")
        ax.plot(right, -delta * (right - 2), color=color, lw=2)
        ax.plot([1, 1], [-delta, delta], color=color, ls=":", lw=1)
    ax.axvline(1, color="#6b4c91", ls="--", lw=1)
    ax.axvline(2, color="#777777", ls=":", lw=1)
    ax.axhline(0, color="#bbbbbb", lw=0.8)
    ax.set(xlim=(0, 2.12), ylim=(-2.15, 2.65),
           xlabel=r"Time $t/\tau$", ylabel=r"Phase relative to $+v$ (rad)",
           xticks=[0, 1, 2], xticklabels=["0", r"$\tau$", r"$2\tau$"])
    ax.text(1, 2.3, r"$180_v^\circ$: phase reflection", ha="center", color="#6b4c91")
    handles, labels = ax.get_legend_handles_labels()
    fig.legend(handles, labels, loc="center", bbox_to_anchor=(0.5, 0.53),
               ncol=5, fontsize=9, frameon=False)
    ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("A pulse reflects phase; free precession keeps its direction", fontsize=15, y=0.98)
    fig.subplots_adjust(left=0.075, right=0.98, top=0.86, bottom=0.16, hspace=0.32, wspace=0.2)
    fig.text(0.5, 0.015,
             r"Ideal $90_u^\circ$ then $180_v^\circ$; static detunings, no relaxation. Coincident arrows use different lengths for visibility.",
             ha="center", fontsize=10)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()