codex/spin-echo-gradient-timing.py

spin-echo-gradient-timing.py
"""Original ideal rectangular-pulse timing diagram; write PNG to the cwd.

Timings are chosen for illustration, not a reconstruction of a published run.
The normalized phase-encoding wavevector includes only applied gradients.
"""
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    duration, first, second, refocus, echo = 2.0, 3.0, 33.0, 30.0, 60.0
    t = np.linspace(-3, 65, 13601)
    gradient = (((t >= first) & (t < first + duration)).astype(float)
                + ((t >= second) & (t < second + duration)).astype(float))
    q = (np.clip((t - first) / duration, 0, 1)
         - np.clip((t - second) / duration, 0, 1))
    integral = np.trapezoid(q**2, t)
    expected = second - first - duration / 3
    assert abs(integral - expected) < 1e-5, (integral, expected)
    fig, axes = plt.subplots(3, 1, figsize=(8, 6), dpi=100, sharex=True,
                             gridspec_kw={"height_ratios": [0.85, 1.2, 1.2]},
                             facecolor="white")
    fig.subplots_adjust(left=0.14, right=0.97, top=0.80, bottom=0.20, hspace=0.3)
    fig.suptitle("Encoding displacement with a refocused gradient pair",
                 fontsize=14, weight="bold", y=0.965, color="#172330")
    fig.text(0.5, 0.905, "Idealized chosen timings; both physical gradient pulses have the same sign",
             ha="center", fontsize=10.5, color="#45535d")
    rf, grad, phase = axes
    rf.hlines(0, -3, 65, color="#7b8992", linewidth=1)
    rf.vlines([0, refocus], [0, 0], [1, 1.8], color="#273c50", linewidth=3)
    rf.text(0, 1.18, "90°", ha="center", fontsize=11)
    rf.text(refocus, 1.98, "180°", ha="center", fontsize=11)
    rf.plot(echo, 0, "o", color="#973c65", markersize=5)
    rf.text(echo, 0.40, "echo", ha="center", fontsize=11, color="#973c65")
    rf.set_ylim(-0.2, 2.5)
    rf.set_yticks([])
    rf.set_ylabel("RF pulses", fontsize=11)
    grad.plot(t, gradient, color="#216c9b", linewidth=2)
    grad.fill_between(t, 0, gradient, color="#216c9b", alpha=0.17)
    grad.annotate("", (second, 1.70), (first, 1.70),
                  arrowprops={"arrowstyle": "<->", "color": "#273c50"})
    grad.text((first + second) / 2, 1.78, r"$\Delta=30\,\mathrm{ms}$",
              ha="center", fontsize=10)
    grad.annotate("", (first + duration, 1.15), (first, 1.15),
                  arrowprops={"arrowstyle": "<->", "color": "#273c50"})
    grad.text(first + duration / 2, 1.26, r"$\delta=2\,\mathrm{ms}$",
              ha="center", fontsize=10)
    grad.set_ylim(-0.15, 2.15)
    grad.set_yticks([0, 1], ["0", "g"])
    grad.set_ylabel("Applied\ngradient", fontsize=11)
    phase.plot(t, q, color="#b45335", linewidth=2.3)
    phase.text(18, 1.16, r"$q=\gamma g\delta$", ha="center", fontsize=11,
               color="#93432b")
    phase.set_ylim(-0.15, 1.55)
    phase.set_yticks([0, 1])
    phase.set_ylabel(r"$q(t)/(\gamma g\delta)$", fontsize=11)
    phase.set_xlabel("Time (ms)", fontsize=11)
    for ax in axes:
        ax.axvline(refocus, color="#a0aab1", linewidth=0.8, linestyle=":")
        ax.axvline(echo, color="#a0aab1", linewidth=0.8, linestyle=":")
        ax.set_xlim(-3, 65)
        ax.spines[["top", "right"]].set_visible(False)
    fig.text(0.5, 0.082,
             r"The 180° pulse reverses the encoding sign; the second gradient returns $q(t)$ to zero.",
             ha="center", fontsize=10, color="#45535d")
    fig.text(0.5, 0.035,
             r"For free diffusion: $b=\int q(t)^2dt=(\gamma g\delta)^2(\Delta-\delta/3)$; $S/S_0=e^{-bD}$.",
             ha="center", fontsize=10, color="#45535d")
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()