codex/achromatic-doublet-power-cancellation.py

achromatic-doublet-power-cancellation.py
"""Generate achromatic-doublet-power-cancellation.png in the working directory.

Run from _media with Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5.
Original signed-power diagram. Phi_i(q) = Phi_i,d * (1 + q/V_i).
The exactly flat sum belongs only to this first-order synthetic model.
"""

from pathlib import Path

import matplotlib

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


def main():
    q = np.linspace(-.5, .5, 201)
    crown = 10 * (1 + q / 60)
    flint = -5 * (1 + q / 30)
    total = crown + flint
    assert np.allclose(total, 5, atol=2e-15, rtol=0)
    assert np.allclose(crown[[0, -1]], [119 / 12, 121 / 12])
    assert np.allclose(flint[[0, -1]], [-59 / 12, -61 / 12])
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, (power, change) = plt.subplots(1, 2, figsize=(10.8, 5.4), dpi=110)
    fig.subplots_adjust(left=.075, right=.975, bottom=.33, top=.77, wspace=.30)
    green, purple, black = "#277b50", "#8e4395", "#222222"
    curves = [(crown, crown - 10, green, "-", "Crown: +10 D, V = 60"),
              (flint, flint + 5, purple, "--", "Flint: −5 D, V = 30"),
              (total, total - 5, black, "-.", "Sum: +5 D")]
    for values, shifts, colour, style, label in curves:
        power.plot(q, values, color=colour, linestyle=style, lw=2.5, label=label)
        change.plot(q, shifts, color=colour, linestyle=style, lw=2.5)
    for axis in (power, change):
        axis.set_xlim(-.53, .53)
        axis.set_xticks([-.5, 0, .5], ["−0.5\nRed", "0\nReference", "+0.5\nBlue"])
        axis.set_xlabel("Synthetic colour coordinate q", labelpad=8)
        axis.grid(alpha=.18)
        axis.axvline(0, color="0.65", lw=.8, linestyle=":")
    power.axhline(0, color="0.6", lw=.8)
    power.set(ylim=(-6.3, 11.5), yticks=[-5, 0, 5, 10],
              ylabel="Signed optical power (D)", title="Element powers add to +5 D")
    power.text(0, 10.6, "Positive element", ha="center", color=green, fontsize=10)
    power.text(0, 5.6, "Flat only in this first-order model", ha="center", fontsize=10)
    power.text(0, -4.4, "Negative element", ha="center", color=purple, fontsize=10)
    change.set(ylim=(-.116, .116), yticks=[-.1, -.05, 0, .05, .1],
               ylabel="Change from reference power (D)",
               title="Magnified: opposite colour changes")
    change.text(-.47, .077, r"Flint slope: $-1/6$ D per q", color=purple, fontsize=10,
                bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
    change.text(-.47, -.105, r"Crown slope: $+1/6$ D per q", color=green, fontsize=10,
                bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
    change.annotate("Sum change = 0", xy=(.36, 0), xytext=(.03, .029),
                    arrowprops={"arrowstyle": "->", "color": black}, fontsize=10,
                    bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
    fig.suptitle("Achromatic doublet: cancel colour change, retain focusing power",
                 fontsize=15, y=.965)
    fig.text(.5, .867, r"$\Phi_c(q)=10(1+q/60)$ D,  $\Phi_f(q)=-5(1+q/30)$ D;  $10/60-5/30=0$", ha="center", fontsize=12)
    handles, labels = power.get_legend_handles_labels()
    fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(.5, .10),
               ncol=3, frameon=False, fontsize=10)
    fig.text(.5, .073, "Toy dispersion, not measured glass. Thin elements in contact in air; total focal length = 0.2 m.",
             ha="center", fontsize=10)
    fig.text(.5, .027, "Real achromats correct two selected wavelengths and can retain secondary spectrum.",
             ha="center", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()