codexcircle-stereographic-charts-unfolding.py
circle-stereographic-charts-unfolding.py
"""Two circle charts and their real coordinates; Python 3.14, matplotlib 3.10."""
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 = plt.figure(figsize=(10, 7.4), facecolor="white")
grid = fig.add_gridspec(2, 2, height_ratios=[3.5, 1], hspace=0.15, wspace=0.24)
blue, orange, gray = "#176b9b", "#d7651d", "#78828b"
theta = np.linspace(0, 2 * np.pi, 900)
point = np.array([3 / 5, 4 / 5])
for col, (pole, value, variable, title) in enumerate([
(-1, 0.5, "t", r"$U=S^1\setminus\{W\}$: omit west"),
(1, 2, "u", r"$V=S^1\setminus\{E\}$: omit east"),
]):
ax = fig.add_subplot(grid[0, col])
ax.set_aspect("equal")
ax.set_xlim(-1.45, 1.5)
ax.set_ylim(-1.35, 2.5)
ax.axis("off")
ax.set_title(title, fontsize=13, pad=12)
ax.axvline(0, color=gray, lw=1, zorder=0)
ax.text(0.06, 2.3, "$x=0$", color=gray, fontsize=10)
ax.plot(np.cos(theta), np.sin(theta), color=blue, lw=3)
# The straight ray through the pole intersects x=0 at the chart value.
intercept = np.array([0, value])
end = point if pole == -1 else intercept
ax.plot([pole, end[0]], [0, end[1]], "--", color=orange, lw=1.8)
ax.scatter([pole], [0], s=90, facecolors="white", edgecolors=blue, lw=2, zorder=5)
ax.scatter([-pole], [0], s=35, color=blue, zorder=4)
for x, label in [(-1, "$W$"), (1, "$E$")]:
ax.text(x, -0.2, label, ha="center")
ax.scatter(*point, s=48, color=orange, zorder=5)
ax.annotate("$P=(3/5,4/5)$", point, xytext=(0.18, 1.32),
arrowprops={"arrowstyle": "-", "color": gray}, fontsize=11)
ax.scatter(*intercept, s=70, marker="D", color=orange, zorder=5)
ax.text(-0.1, value + 0.12, f"${variable}={value:g}$", ha="right", color=orange)
denominator = "1+x" if variable == "t" else "1-x"
ax.text(0, -1.3, rf"${variable}=\dfrac{{y}}{{{denominator}}}$", ha="center", fontsize=16)
line = fig.add_subplot(grid[1, col])
line.set_xlim(-2.65, 2.65)
line.set_ylim(-0.7, 0.75)
line.axis("off")
line.annotate("", xy=(2.55, 0), xytext=(-2.55, 0),
arrowprops={"arrowstyle": "<->", "lw": 2, "color": blue})
for tick in [-2, -1, 0, 1, 2]:
line.plot([tick, tick], [-0.05, 0.05], color=blue)
line.text(tick, -0.3, str(tick), ha="center", fontsize=11)
line.scatter([value], [0], s=55, color=orange, zorder=5)
line.text(value, 0.23, rf"$P\mapsto {value:g}$", ha="center", color=orange)
line.text(0, -0.65, f"{variable}=0 is {'east E' if variable == 't' else 'west W'}", ha="center", fontsize=11)
line.text(2.5, 0.23, f"${variable}$", ha="center", color=blue)
fig.suptitle("One circle, two coordinate descriptions", fontsize=17, y=0.98)
fig.text(0.5, 0.043, r"Overlap: $t\ne0$, $u\ne0$ and $u=1/t$; here $1/2\leftrightarrow2$.", ha="center", fontsize=13)
fig.text(0.5, 0.012, "Both chart images are whole real lines; together their domains cover the circle.", ha="center", fontsize=11)
fig.subplots_adjust(top=0.88, bottom=0.12, left=0.05, right=0.96)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()