codex/polarimetry-mixtures-and-analyzer-intensity.py

polarimetry-mixtures-and-analyzer-intensity.py
"""Original additive enantiomer/Malus model; Python 3.14, matplotlib 3.10.7.

Uses numpy 2.3.5. Run from _media; emits this script's basename.png to CWD.
Parameters describe an unnamed hypothetical pair, not experimental data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, (ax, scan) = plt.subplots(1, 2, figsize=(11, 6.4), dpi=100, facecolor="white")
    fig.subplots_adjust(left=0.075, right=0.98, bottom=0.26, top=0.77, wspace=0.35)
    gray = "#475569"
    pure_rotation = 40 * 1 * 0.200
    cases = [(1.0, "Pure (+): 8°", "#2463A6", "-"),
             (0.75, "75:25 pair: 4°", "#C45B15", "--"),
             (0.50, "Racemate: 0°", "#16805D", ":")]
    fig.suptitle("Enantiomer balance shifts the analyzer curve", fontsize=20, y=0.965)
    fig.text(0.5, 0.895, "Original stipulated model · unnamed pair · no measured data",
             ha="center", color=gray)
    fig.text(0.5, 0.84, "[α]₊ = +40 deg mL/(g dm)     l = 1 dm     total c = 0.200 g/mL",
             ha="center", fontsize=12)
    fraction = np.linspace(0, 1, 401)
    ax.plot(fraction, pure_rotation*(2*fraction-1), color=gray, lw=2)
    ax.axhline(0, color="#CBD5E1", lw=1)
    ax.set(xlim=(-0.03, 1.05), ylim=(-9.5, 10.5),
           xlabel="Positive-member mole fraction within pair, x₊",
           ylabel="Optical rotation α (degrees)",
           xticks=[0, 0.25, 0.50, 0.75, 1], yticks=[-8, -4, 0, 4, 8])
    ax.text(0.07, 8.8, "α = 8° (2x₊ − 1)", color=gray, fontsize=13)
    labels = {1.0: (-0.05, 1.1, "100:0"), 0.75: (-0.02, 1.3, "75:25"),
              0.50: (0.03, -1.9, "50:50")}
    theta = np.linspace(0, 180, 3601)
    zoom = scan.inset_axes([0.38, 0.17, 0.58, 0.40])
    for xplus, label, color, style in cases:
        alpha = pure_rotation*(2*xplus-1)
        ax.scatter(xplus, alpha, s=65, color=color, zorder=4)
        dx, dy, ratio = labels[xplus]
        ax.text(xplus+dx, alpha+dy, ratio, color=color, ha="right" if xplus == 1 else "left")
        curve = np.cos(np.deg2rad(theta-alpha))**2
        scan.plot(theta, curve, color=color, ls=style, lw=2, label=label)
        local_theta = np.linspace(86, 102, 321)
        zoom.plot(local_theta, np.cos(np.deg2rad(local_theta-alpha))**2,
                  color=color, ls=style, lw=2)
        zoom.plot(90+alpha, 0, "o", ms=4, color=color, clip_on=False)
    scan.set(xlim=(0, 180), ylim=(-0.02, 1.05), xticks=[0, 45, 90, 135, 180],
             yticks=[0, 0.5, 1], xlabel="Analyzer angle θ (degrees; clockwise)",
             ylabel="Normalized intensity I / I₀")
    scan.legend(frameon=False, loc="upper center", fontsize=10.5)
    zoom.set(xlim=(86, 102), ylim=(-0.001, 0.045), xticks=[90, 94, 98], yticks=[0, 0.04])
    zoom.set_title("Extinction minima (zoom)", fontsize=9.5)
    zoom.tick_params(labelsize=9)
    zoom.set_facecolor("white")
    for plot in [ax, scan]:
        plot.spines[["top", "right"]].set_visible(False)
        plot.grid(alpha=0.16)
    fig.text(0.5, 0.125, "I / I₀ = cos²(θ − α)     ·     same solvent, 20°C and 589 nm",
             ha="center", fontsize=13)
    fig.text(0.5, 0.065, "I₀ is the intensity just before the analyzer; each curve is normalized to its own I₀.",
             ha="center", color=gray, fontsize=11)
    fig.text(0.5, 0.025, "Additive linear pair response and preserved linear polarization are assumptions.",
             ha="center", color=gray, fontsize=11)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()