codex/givens-qr-row-elimination.py

givens-qr-row-elimination.py
"""Draw a two-rotation QR example; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media; the PNG is written to the current working directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


def draw_matrix(ax, entries, title, active_rows=(), target=None, zeros=()):
    for row in active_rows:
        ax.add_patch(Rectangle((-0.42, 1.62 - row), 1.84, 0.76,
                               facecolor="#e7effa", edgecolor="none"))
    for row, values in enumerate(entries):
        for col, value in enumerate(values):
            color = "#1c7254" if (row, col) in zeros else "#172b42"
            if (row, col) == target:
                ax.add_patch(Rectangle((col - 0.35, 1.66 - row), 0.7, 0.68,
                                       facecolor="#ffe0ba", edgecolor="#d57d24", lw=1.2))
            ax.text(col, 2 - row, value, ha="center", va="center", fontsize=19, color=color)
    for x, side in [(-0.6, 1), (1.6, -1)]:
        ax.plot([x + 0.15 * side, x, x, x + 0.15 * side],
                [2.48, 2.48, -0.48, -0.48], color="#172b42", lw=1.8)
    ax.set(xlim=(-0.85, 1.85), ylim=(-1.2, 3.4))
    ax.text(0.5, 3.04, title, ha="center", va="center", fontsize=15)
    ax.axis("off")


def main():
    a = np.array([[3., 0.], [4., 5.], [0., 2.]])
    g1 = np.array([[0.6, 0.8, 0.], [-0.8, 0.6, 0.], [0., 0., 1.]])
    t = np.hypot(3., 2.)
    g2 = np.array([[1., 0., 0.], [0., 3/t, 2/t], [0., -2/t, 3/t]])
    assert np.allclose(g1 @ a, [[5., 4.], [0., 3.], [0., 2.]])
    assert np.allclose(g2 @ g1 @ a, [[5., 4.], [0., t], [0., 0.]])
    fig, axes = plt.subplots(1, 3, figsize=(9, 4.3), dpi=120, facecolor="white")
    draw_matrix(axes[0], [["3", "0"], ["4", "5"], ["0", "2"]], r"$A$",
                active_rows=(0, 1), target=(1, 0), zeros=((2, 0),))
    draw_matrix(axes[1], [["5", "4"], ["0", "3"], ["0", "2"]], r"$G_1A$",
                active_rows=(1, 2), target=(2, 1), zeros=((1, 0), (2, 0)))
    draw_matrix(axes[2], [["5", "4"], ["0", r"$\sqrt{13}$"], ["0", "0"]],
                r"$R=G_2G_1A$", zeros=((1, 0), (2, 0), (2, 1)))
    notes = ["Mix rows 1 and 2\nEliminate the 4", "Mix rows 2 and 3\nEliminate the 2",
             "Zeros below the diagonal\nEarlier zeros survive"]
    for ax, note in zip(axes, notes):
        ax.text(0.5, -0.85, note, ha="center", va="center", fontsize=11, linespacing=1.5)
    fig.suptitle("QR by local row rotations", fontsize=16, y=0.98)
    fig.subplots_adjust(top=0.85, bottom=0.2, left=0.02, right=0.98, wspace=0.24)
    for x, label in [(0.337, r"$G_1$"), (0.666, r"$G_2$")]:
        fig.text(x, 0.53, r"$\longrightarrow$", fontsize=22, ha="center")
        fig.text(x, 0.60, label, fontsize=12, ha="center")
    fig.text(0.5, 0.055, "Blue: rows mixed next    Orange: entry eliminated next    Green: zero",
             ha="center", fontsize=10, color="#33465b")
    fig.savefig(Path.cwd() / f"{Path(__file__).stem}.png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()