codexpoint-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()