codexsingular-value-ellipse.py
singular-value-ellipse.py
"""Map a unit circle to an ellipse; write an opaque PNG to CWD.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def arrow(ax, end, color):
ax.annotate("", end, (0, 0), arrowprops={
"arrowstyle": "->", "color": color, "lw": 2.6,
"shrinkA": 0, "shrinkB": 0, "mutation_scale": 16}, zorder=5)
def main():
matrix = np.array([[3., 4.], [-4 / 5, 3 / 5]])
right = np.array([[3., -4.], [4., 3.]]) / 5
sigma = np.diag([5., 1.])
assert np.allclose(right.T @ right, np.eye(2))
assert np.allclose(sigma @ right.T, matrix)
assert np.allclose(matrix @ right, sigma)
assert np.allclose(matrix.T @ matrix, np.array([[241, 288], [288, 409]]) / 25)
assert np.allclose(np.linalg.eigvalsh(matrix.T @ matrix), [1, 25])
assert np.allclose(np.linalg.eigvals(matrix), (9 + np.array([1j, -1j]) * np.sqrt(44)) / 5)
mixed = right @ np.array([1., 1.]) / np.sqrt(2)
assert np.isclose(np.linalg.norm(mixed), 1)
assert np.isclose(np.linalg.norm(matrix @ mixed), np.sqrt(13))
epsilon = 0.01
weak_matrix = np.diag([5., epsilon]) @ right.T
assert np.allclose(weak_matrix, [[3, 4], [-0.008, 0.006]])
assert np.isclose(np.linalg.cond(weak_matrix), 500)
assert np.allclose(np.linalg.solve(weak_matrix, [0, 0.001]), [-0.08, 0.06])
angle = np.linspace(0, 2 * np.pi, 600)
circle = right @ np.array([np.cos(angle), np.sin(angle)])
ellipse = matrix @ circle
assert np.allclose(ellipse[0] ** 2 / 25 + ellipse[1] ** 2, 1)
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.4),
gridspec_kw={"width_ratios": [1, 3.7]})
fig.subplots_adjust(left=0.055, right=0.98, bottom=0.22, top=0.73, wspace=0.22)
blue, orange, ink = "#2365a0", "#c05621", "#253b46"
for ax, limit in zip(axes, [1.55, 5.75]):
ax.set_facecolor("white")
ax.set_aspect("equal")
ax.set_xlim(-limit, limit)
ax.set_ylim(-1.65, 1.65)
ax.axhline(0, color="#bbbbbb", lw=0.8)
ax.axvline(0, color="#bbbbbb", lw=0.8)
ax.grid(alpha=0.15)
ax.spines[["top", "right"]].set_visible(False)
ax.set_yticks([-1, 0, 1])
ax.plot(0, 0, "o", color=ink, ms=4, zorder=6)
source, target = axes
source.set_title("Input: unit circle", fontsize=13, pad=10)
target.set_title("Output: ellipse with semiaxes 5 and 1", fontsize=13, pad=10)
source.set_xticks([-1, 0, 1])
target.set_xticks([-5, -3, -1, 0, 1, 3, 5])
source.plot(*circle, color=ink, lw=2)
target.plot(*ellipse, color=ink, lw=2)
for j, color in enumerate([blue, orange]):
arrow(source, right[:, j], color)
arrow(target, sigma[:, j], color)
source.plot(*(-right[:, j]), "o", color=color, ms=5)
target.plot(*(-sigma[:, j]), "o", color=color, ms=5)
source.text(0.7, 1.03, r"$v_1$", color=blue)
source.text(-1.22, 0.75, r"$v_2$", color=orange)
target.text(2.5, 0.15, r"$Av_1=5u_1$", color=blue, ha="center")
target.text(0.25, 1.12, r"$Av_2=u_2$", color=orange)
target.text(-3.8, -1.30, r"$y_1^2/25+y_2^2=1$", color=ink)
fig.suptitle("Singular directions: perpendicular in, perpendicular out", fontsize=17, y=0.97)
fig.text(0.5, 0.84, "A = [[3, 4], [−4/5, 3/5]] " + r"$\sigma_1=5,\ \sigma_2=1$",
ha="center", fontsize=14)
fig.text(0.5, 0.09, "Each panel has equal horizontal and vertical units. Colored endpoints correspond.",
ha="center", fontsize=11)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=130,
facecolor="white", transparent=False)
plt.close(fig)
print("Checked: A=UΣVᵀ; Av₁=5u₁; Av₂=u₂; eigenvalues of AᵀA = 25,1; ellipse equation; A has no real eigenvalues.")
if __name__ == "__main__":
main()