codex/point-mass-lens-image-mapping-and-alignment.py

point-mass-lens-image-mapping-and-alignment.py
"""Draw signed point-lens roots and exact alignment; output to the CWD.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
This is a theoretical diagram, not an observed sky image.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig, (ax, ring) = plt.subplots(1, 2, figsize=(10.0, 4.7), facecolor="white", gridspec_kw={"width_ratios": [1.3, 1.0]})
    u = 0.6
    roots = (u + np.array([1.0, -1.0]) * np.sqrt(u**2 + 4)) / 2
    for start, end in [(-2.6, -0.15), (0.15, 2.6)]:
        x = np.linspace(start, end, 500)
        ax.plot(x, x - 1 / x, color="#263f56", lw=2)
    ax.axhline(u, color="#7d4394", lw=1.8, label=r"Source $u=0.6$")
    ax.axhline(0, color="#a1a1a1", lw=0.7)
    ax.axvline(0, color="#a1a1a1", lw=0.7)
    for r, color in zip(roots, ["#1268a1", "#bc472d"]):
        ax.scatter(r, u, s=65, color=color, zorder=5)
        ax.vlines(r, -3, u, color=color, linestyles="--", lw=1.2)
    ax.annotate("Outer image\n$x_+=1.344$\nPositive parity", (roots[0], u), (1.2, -1.9), color="#1268a1", fontsize=11, arrowprops={"arrowstyle": "->", "color": "#1268a1"})
    ax.annotate("Inner image\n$x_-=-0.744$\nNegative parity", (roots[1], u), (-2.48, 1.6), color="#bc472d", fontsize=11, arrowprops={"arrowstyle": "->", "color": "#bc472d"})
    ax.set_xlim(-2.6, 2.6)
    ax.set_ylim(-3, 3.5)
    ax.set_xticks([-2, -1, 0, 1, 2])
    ax.set_xlabel(r"Signed image angle $x=\theta/\theta_E$")
    ax.set_ylabel(r"Signed source angle $u=\beta/\theta_E$")
    ax.set_title(r"Offset source: $u=x-1/x$", fontsize=14, pad=13)
    ax.legend(loc="upper right", fontsize=10, frameon=False)
    ax.spines[["top", "right"]].set_visible(False)
    t = np.linspace(0, 2 * np.pi, 500)
    ring.plot(np.cos(t), np.sin(t), color="#1268a1", lw=3)
    ring.axhline(0, color="#cccccc", lw=0.7)
    ring.axvline(0, color="#cccccc", lw=0.7)
    ring.scatter(0, 0, color="#7d4394", s=110, marker="*", zorder=5)
    ring.scatter([-1, 1], [0, 0], s=50, color="#bc472d", zorder=5)
    ring.annotate("", (np.cos(0.55), np.sin(0.55)), (0, 0), arrowprops={"arrowstyle": "->", "color": "#444444"})
    ring.text(0.42, 0.38, r"$\theta_E$", color="#444444")
    ring.text(0.02, -0.25, "Aligned source direction", ha="center", color="#7d4394", fontsize=10)
    ring.text(0, -1.35, "Red points: a diameter cut\nBlue circle: all image directions", ha="center", fontsize=10, linespacing=1.5)
    ring.set_title(r"Exact alignment: $\boldsymbol{\beta}=0$", fontsize=14, pad=13)
    ring.set_xlim(-1.65, 1.65)
    ring.set_ylim(-1.65, 1.65)
    ring.set_aspect("equal")
    ring.set_xlabel(r"$\theta_1/\theta_E$")
    ring.set_ylabel(r"$\theta_2/\theta_E$")
    ring.set_xticks([-1, 0, 1])
    ring.set_yticks([-1, 0, 1])
    ring.spines[["top", "right"]].set_visible(False)
    fig.suptitle("Point-mass gravitational lens: two images or an Einstein ring", fontsize=16, y=0.97)
    fig.text(0.5, 0.015, "Ideal geometric-optics model; angles are normalized by the Einstein angular radius.", ha="center", fontsize=10)
    fig.subplots_adjust(left=0.075, right=0.98, bottom=0.17, top=0.79, wspace=0.33)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()