codexchange-of-basis-coordinates.py
change-of-basis-coordinates.py
"""Show one fixed vector in old and new bases; write the 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, start, end, color, lw=2.4, style="-", zorder=4):
ax.annotate("", end, start,
arrowprops={"arrowstyle": "->", "color": color,
"lw": lw, "linestyle": style, "shrinkA": 0,
"shrinkB": 0, "mutation_scale": 16}, zorder=zorder)
def main():
p = np.array([[1., 1.], [1., -1.]])
inverse = np.linalg.inv(p)
vector = np.array([2., 0.])
coordinates = inverse @ vector
assert np.allclose(coordinates, [1, 1])
assert np.allclose(p @ coordinates, vector)
assert np.allclose(inverse, p / 2)
assert np.isclose(np.linalg.det(p), -2)
assert np.allclose(inverse @ [5, 5], [5, 0])
old_map = np.diag([2., 3.])
new_map = inverse @ old_map @ p
assert np.allclose(new_map, [[2.5, -.5], [-.5, 2.5]])
assert np.allclose(new_map @ coordinates, [2, 2])
assert np.allclose(p @ (new_map @ coordinates), [4, 0])
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(9.2, 5.7), facecolor="white")
fig.subplots_adjust(left=0.06, right=0.97, bottom=0.21, top=0.79, wspace=0.18)
blue, orange, ink = "#2365a0", "#c05621", "#183a2a"
for ax in axes:
ax.set_facecolor("white")
ax.set_aspect("equal")
ax.set_xlim(-1.1, 3.1)
ax.set_ylim(-1.5, 1.8)
ax.set_xticks([])
ax.set_yticks([])
for spine in ax.spines.values():
spine.set_visible(False)
ax.plot(0, 0, "o", color=ink, ms=4, zorder=6)
ax.text(-0.07, -0.12, "O", ha="right", va="top", color=ink)
arrow(ax, (0, 0), vector, ink, lw=3.3, zorder=3)
ax.plot(*vector, "o", color=ink, ms=6, zorder=6)
ax.text(2.08, 0.10, r"$z=(2,0)$", color=ink, fontsize=13)
old, new = axes
old.set_title("Old basis: horizontal and vertical", fontsize=14, pad=15)
for k in range(-3, 5):
old.axhline(k, color="#e0e0e0", lw=0.8, zorder=0)
old.axvline(k, color="#e0e0e0", lw=0.8, zorder=0)
old.axhline(0, color="#aaaaaa", lw=1, zorder=1)
old.axvline(0, color="#aaaaaa", lw=1, zorder=1)
arrow(old, (0, 0), (1, 0), blue, lw=2.4, zorder=5)
arrow(old, (0, 0), (0, 1), orange)
old.text(0.75, -0.27, r"$e_1=(1,0)$", color=blue, ha="center")
old.text(0.12, 1.08, r"$e_2=(0,1)$", color=orange)
old.text(1.55, 0.18, r"$2e_1$", color=ink, ha="center")
new.set_title("New basis: two diagonal directions", fontsize=14, pad=15)
t = np.linspace(-6, 6, 2)
for k in range(-4, 5):
along_v = k * p[:, 0, None] + p[:, 1, None] * t
along_u = p[:, 0, None] * t + k * p[:, 1, None]
new.plot(*along_v, color="#e0e0e0", lw=0.8, zorder=0)
new.plot(*along_u, color="#e0e0e0", lw=0.8, zorder=0)
new.plot(t, t, color="#aaaaaa", lw=1, zorder=1)
new.plot(t, -t, color="#aaaaaa", lw=1, zorder=1)
arrow(new, (0, 0), p[:, 0], blue)
arrow(new, (0, 0), p[:, 1], orange)
arrow(new, p[:, 0], vector, orange, style="--", lw=2)
new.text(0.88, 1.18, r"$u=(1,1)$", color=blue, ha="center")
new.text(1.1, -1.18, r"$v=(1,-1)$", color=orange, ha="center")
new.text(1.68, 0.62, r"$+v$", color=orange)
fig.suptitle("Change of basis: the arrow stays fixed", fontsize=19, y=0.96)
fig.text(0.5, 0.87, "Same origin • Same endpoint • Same geometric scale",
ha="center", fontsize=12)
for ax, label in zip(axes, [r"$z=2e_1+0e_2$ old coordinates: $(2,0)$",
r"$z=u+v$ new coordinates: $(1,1)$"]):
bounds = ax.get_position()
fig.text((bounds.x0 + bounds.x1) / 2, 0.15, label, ha="center", fontsize=12)
fig.text(0.5, 0.065, "P converts new coordinates to old; P⁻¹ converts old to new.",
ha="center", fontsize=12)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=130,
facecolor="white", transparent=False)
plt.close(fig)
print("Checked: det(P)=-2; old (2,0) ↔ new (1,1); P⁻¹AP; new output (2,2) ↔ old (4,0).")
if __name__ == "__main__":
main()