codex/raman-vibrational-levels-and-spectrum.py

raman-vibrational-levels-and-spectrum.py
"""Draw molecular Raman energy bookkeeping and a calculated illustrative spectrum.

Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5 (root pyproject.toml).
Integrated radiant line strengths obey the off-resonant harmonic thermal model.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    h, c, kb = 6.62607015e-34, 299792458.0, 1.380649e-23
    excitation_nm, shift_cm, temperature = 532.0, 1000.0, 300.0
    nu0_cm = 1e7 / excitation_nm
    thermal = np.exp(-h * c * 100 * shift_cm / (kb * temperature))
    ratio = ((nu0_cm + shift_cm) / (nu0_cm - shift_cm)) ** 4 * thermal
    fig = plt.figure(figsize=(11, 7.7), dpi=110, facecolor="white")
    gs = fig.add_gridspec(2, 3, height_ratios=(1.15, 1), hspace=.48, wspace=.18)
    for i, (name, initial, final, color, emitted) in enumerate([
        ("Stokes", 0, 1, "#b44d28", r"$h(\nu_0-\nu_v)$"),
        ("Rayleigh", 0, 0, "#59616b", r"$h\nu_0$"),
        ("Anti-Stokes", 1, 0, "#267c9d", r"$h(\nu_0+\nu_v)$"),
    ]):
        ax = fig.add_subplot(gs[0, i])
        ax.set(xlim=(0, 1), ylim=(-.7, 4.3))
        ax.axis("off")
        ax.set_title(name, weight="bold", color=color, pad=16)
        for n in (0, 1):
            ax.plot([.08, .92], [n, n], color="#222222", lw=1.6)
            ax.text(.96, n, f"v = {n}", va="center", fontsize=10)
        virtual = 3.0 + initial
        ax.plot([.2, .82], [virtual, virtual], "--", color="#999999")
        ax.annotate("", xy=(.4, virtual), xytext=(.2, initial),
                    arrowprops={"arrowstyle": "->", "lw": 2, "color": "#5b56a6"})
        ax.annotate("", xy=(.82, final), xytext=(.6, virtual),
                    arrowprops={"arrowstyle": "->", "lw": 2, "color": color})
        ax.text(.08, (initial+virtual)/2, r"$h\nu_0$", color="#5b56a6", ha="right")
        ax.text(.74, (final+virtual)/2, emitted, color=color, ha="left", fontsize=10)
        ax.text(.5, -.48, f"Molecule: v = {initial} → {final}", ha="center", fontsize=10)
    ax = fig.add_subplot(gs[1, :])
    x = np.linspace(-1450, 1450, 18001)
    sigma = 25.0
    normalizer = sigma * np.sqrt(2*np.pi)
    def peak(center, area):
        return area * np.exp(-.5 * ((x-center)/sigma)**2) / normalizer
    stokes = peak(shift_cm, 1)
    anti = peak(-shift_cm, ratio)
    rayleigh = peak(0, 20)
    ax.plot(x, stokes + anti + rayleigh, color="#444444", lw=1.5)
    ax.fill_between(x, stokes, 0, color="#b44d28", alpha=.35)
    ax.fill_between(x, anti, 0, color="#267c9d", alpha=.5)
    ax.set_yscale("log")
    ax.set(xlim=(-1450, 1450), ylim=(1e-5, 1),
           xlabel=r"Signed Raman shift $\widetilde{\nu}_0-\widetilde{\nu}_s$ (cm$^{-1}$)",
           ylabel="Radiant spectral density\n(arbitrary units, log scale)")
    ax.set_xticks([-1000, 0, 1000])
    ax.grid(alpha=.2, axis="y")
    ax.text(-1000, .007, f"Anti-Stokes\n505.13 nm\nArea = {ratio:.5f}", ha="center", color="#267c9d", fontsize=10)
    ax.text(180, .18, "Rayleigh: 532 nm\nArea = 20 (chosen)", ha="left", fontsize=10)
    ax.text(1000, .06, "Stokes\n561.89 nm\nArea = 1", ha="center", color="#b44d28", fontsize=10)
    ax.set_title("Calculated one-mode spectrum: 1000 cm⁻¹, 532 nm excitation, 300 K", loc="left", fontsize=12, pad=12)
    fig.suptitle("Raman scattering exchanges molecular vibrational energy", fontsize=15, weight="bold", y=.98)
    fig.text(.5, .525, "Dashed levels are virtual intermediates, not occupied electronic levels.\n"
             "Arrow energies are schematic; vertical spacing is not to scale.", ha="center", fontsize=10)
    fig.text(.5, .025, "Original model, not a 1928 measurement. Equal Gaussian widths σ = 25 cm⁻¹ are chosen.\n"
             "Radiant anti-Stokes/Stokes area ratio includes the fourth power of the photon-frequency ratio.", ha="center", fontsize=10)
    fig.subplots_adjust(top=.86, bottom=.15, left=.105, right=.91)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()