codexgram-schmidt-vector-steps.py
gram-schmidt-vector-steps.py
"""Show projection subtraction and unit directions; write a PNG to CWD.
Tested with Python 3.14.4, Matplotlib 3.10.7 and NumPy 2.3.5.
Run from _media to create gram-schmidt-vector-steps.png there.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
BLUE, RED, ORANGE, TEAL = "#2465a0", "#9c3042", "#c05a10", "#13796d"
def arrow(ax, end, color, start=(0, 0), dashed=False):
ax.annotate("", xy=end, xytext=start, arrowprops={
"arrowstyle": "->", "color": color, "lw": 2.6,
"linestyle": "--" if dashed else "-", "shrinkA": 0,
"shrinkB": 0, "mutation_scale": 16}, zorder=5)
def corner(ax, origin, direction_a, direction_b):
scale = 0.11
a, b = scale * direction_a, scale * direction_b
points = np.array([origin + a, origin + a + b, origin + b])
ax.plot(*points.T, color="#59636c", lw=1.2, zorder=6)
def main():
v1, v2 = np.array([1., 1.]), np.array([1., 0.])
q1 = v1 / np.linalg.norm(v1)
projection = np.dot(v2, q1) * q1
u2 = v2 - projection
q2 = u2 / np.linalg.norm(u2)
assert np.allclose(projection, [0.5, 0.5], atol=1e-14)
assert np.allclose(u2, [0.5, -0.5], atol=1e-14)
assert np.isclose(np.linalg.norm(u2), 1 / np.sqrt(2), atol=1e-14)
assert np.allclose(np.column_stack([q1, q2]).T @
np.column_stack([q1, q2]), np.eye(2), atol=1e-14)
dependent = np.array([2., 2.])
assert np.allclose(dependent - np.dot(dependent, q1) * q1, 0, atol=1e-14)
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig, axes = plt.subplots(1, 3, figsize=(11, 4.7))
fig.subplots_adjust(left=0.055, right=0.98, top=0.77, bottom=0.20, wspace=0.35)
for ax in axes:
ax.set_aspect("equal")
ax.set_facecolor("white")
ax.set_xlim(-0.25, 1.55)
ax.set_ylim(-1.0, 1.5)
ax.set_xticks([0, 0.5, 1])
ax.set_yticks([-0.5, 0, 0.5, 1])
ax.axhline(0, color="#b8c0c6", lw=0.8)
ax.axvline(0, color="#b8c0c6", lw=0.8)
ax.grid(alpha=0.12)
ax.spines[["top", "right"]].set_visible(False)
ax.tick_params(labelsize=10)
ax.plot(0, 0, "o", color="#253b46", ms=4, zorder=7)
first, second, third = axes
first.set_title("1. Original vectors", fontsize=13, pad=12)
arrow(first, v1, BLUE)
arrow(first, v2, RED)
first.text(0.73, 1.15, r"$v_1=(1,1)$", color=BLUE, fontsize=11)
first.text(0.72, -0.23, r"$v_2=(1,0)$", color=RED, fontsize=11)
second.set_title("2. Subtract the projection", fontsize=13, pad=12)
second.plot([0, 1.22], [0, 1.22], color=BLUE, lw=1, alpha=0.3)
arrow(second, v2, RED)
arrow(second, projection, TEAL)
arrow(second, u2, ORANGE)
arrow(second, v2, ORANGE, start=projection, dashed=True)
corner(second, projection, -q1, q2)
second.text(0.03, 0.67, r"$p=(1/2,1/2)$", color=TEAL, fontsize=11)
second.text(0.54, -0.63, r"$u_2=(1/2,-1/2)$", color=ORANGE, fontsize=11)
second.text(1.06, 0.08, r"$v_2$", color=RED)
second.text(0.57, 1.19, r"$v_2=p+u_2$", fontsize=12)
third.set_title("3. Divide by each length", fontsize=13, pad=12)
angle = np.linspace(0, 2 * np.pi, 300)
third.plot(np.cos(angle), np.sin(angle), color="#7e8993", alpha=0.5, lw=1)
arrow(third, q1, BLUE)
arrow(third, q2, ORANGE)
corner(third, np.zeros(2), q1, q2)
third.text(0.67, 0.83, r"$q_1$", color=BLUE)
third.text(0.80, -0.73, r"$q_2$", color=ORANGE)
third.text(0.00, 1.19, r"$\|q_1\|=\|q_2\|=1$", fontsize=12)
fig.suptitle("Gram–Schmidt: remove overlap, then set length to one", fontsize=17, y=0.96)
fig.text(0.5, 0.08, "All panels use equal horizontal and vertical units. "
"The dashed arrow is the residual translated to the projection tip.",
ha="center", fontsize=11)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120,
facecolor="white", transparent=False)
plt.close(fig)
print("Checked: p=(1/2,1/2); u2=(1/2,-1/2); Q.T Q=I; dependent input has zero remainder.")
if __name__ == "__main__":
main()