codex/eigenvector-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()