codexeigenvector-geometry.py
eigenvector-geometry.py
"""Draw eigenvector-geometry.png in cwd; Python 3.14, NumPy, Matplotlib."""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import numpy as np
BLUE = "#1765a8"
PURPLE = "#9a357a"
ORANGE = "#bd5d00"
def arrow(ax, vector, color, dashed=False):
ax.annotate(
"",
xy=vector,
xytext=(0, 0),
arrowprops={
"arrowstyle": "->",
"color": color,
"lw": 2.5 if not dashed else 1.7,
"linestyle": "--" if dashed else "-",
"mutation_scale": 16,
"shrinkA": 0,
"shrinkB": 0,
},
zorder=4 if dashed else 3,
)
def setup(ax, limit, ticks):
ax.set_aspect("equal")
ax.set_xlim(-limit, limit)
ax.set_ylim(-limit, limit)
ax.set_xticks(ticks)
ax.set_yticks(ticks)
ax.grid(color="#e5e8eb", lw=0.7)
ax.axhline(0, color="#a2aab2", lw=0.9)
ax.axvline(0, color="#a2aab2", lw=0.9)
ax.set_xlabel("x", labelpad=1)
ax.set_ylabel("y", rotation=0, labelpad=6)
for spine in ax.spines.values():
spine.set_visible(False)
def main():
plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
fig, axes = plt.subplots(1, 3, figsize=(11, 4.8), facecolor="white")
fig.subplots_adjust(left=0.055, right=0.98, bottom=0.17, top=0.77, wspace=0.30)
fig.suptitle("Eigenvectors follow a line; other vectors can turn", y=0.97, fontsize=16)
fig.legend(
handles=[
Line2D([0], [0], color="#4a5056", ls="--", lw=2, label="Input vector"),
Line2D([0], [0], color="#4a5056", lw=2.5, label="Image of that vector"),
],
loc="upper center",
bbox_to_anchor=(0.5, 0.91),
ncol=2,
frameon=False,
)
ax = axes[0]
setup(ax, 3.6, [-3, 0, 3])
ax.set_title("Symmetric stretch\nA(x, y) = (2x + y, x + 2y)")
a = np.array([[2, 1], [1, 2]])
diagonal = np.array([-3.6, 3.6])
ax.plot(diagonal, diagonal, color=BLUE, alpha=0.25, lw=2)
ax.plot(diagonal, -diagonal, color=PURPLE, alpha=0.25, lw=2)
for v, color in [(np.array([1, 1]), BLUE), (np.array([1, -1]), PURPLE), (np.array([1, 0]), ORANGE)]:
arrow(ax, a @ v, color)
arrow(ax, v, color, dashed=True)
ax.text(2.15, 3.12, r"$Av_3=3v_3$", color=BLUE, fontsize=11)
ax.text(0.9, -1.65, r"$Av_1=v_1$", color=PURPLE, fontsize=11)
ax.text(2.05, 0.55, "(2, 1)", color=ORANGE, fontsize=10)
ax.text(0.02, 1.3, "(1, 1)", color=BLUE, fontsize=10)
ax.text(0.5, -0.53, "(1, 0)", color=ORANGE, fontsize=10)
ax = axes[1]
setup(ax, 1.7, [-1, 0, 1])
ax.set_title("Quarter-turn rotation\nR(x, y) = (−y, x)")
rotation = np.array([[0, -1], [1, 0]])
for v, color in [(np.array([1.0, 0]), BLUE), (np.array([0.8, 0.8]), ORANGE)]:
arrow(ax, rotation @ v, color)
arrow(ax, v, color, dashed=True)
ax.text(1.02, -0.27, "v", color=BLUE)
ax.text(0.13, 1.05, "Rv", color=BLUE)
ax.text(-1.12, 1.02, "Rw", color=ORANGE)
ax.text(0.92, 0.87, "w", color=ORANGE)
ax.text(0.20, 0.21, "90°", color=BLUE, fontsize=10)
ax = axes[2]
setup(ax, 1.7, [-1, 0, 1])
ax.set_title("Shear\nS(x, y) = (x + y, y)")
shear = np.array([[1, 1], [0, 1]])
ax.axhline(0, color=BLUE, alpha=0.25, lw=3)
e1, e2 = np.eye(2)
arrow(ax, shear @ e1, BLUE)
arrow(ax, e1, BLUE, dashed=True)
arrow(ax, shear @ e2, ORANGE)
arrow(ax, e2, ORANGE, dashed=True)
ax.text(0.54, -0.35, r"$Se_1=e_1$", color=BLUE)
ax.text(-0.45, 1.10, r"$e_2$", color=ORANGE)
ax.text(1.02, 1.02, r"$Se_2$", color=ORANGE)
for ax, caption in zip(axes, ["Two independent eigendirections", "No real eigendirection", "Only one eigendirection: y = 0"]):
ax.text(0.5, -0.24, caption, transform=ax.transAxes, ha="center", fontsize=11)
fig.savefig(Path.cwd() / "eigenvector-geometry.png", dpi=110, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()