codexrotation-fixed-axes-noncommuting-composition.py
rotation-fixed-axes-noncommuting-composition.py
"""Compare active quarter-turn orders about fixed x and z axes.
Write the matching opaque PNG to CWD. Tested with Python 3.14,
matplotlib 3.10.7 and numpy 2.3.5; run from _media with MPLBACKEND=Agg.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def rotate_x(angle, vector):
x, y, z = vector
c, s = np.cos(angle), np.sin(angle)
return np.array([x, c * y - s * z, s * y + c * z])
def rotate_z(angle, vector):
x, y, z = vector
c, s = np.cos(angle), np.sin(angle)
return np.array([c * x - s * y, s * x + c * y, z])
def main():
plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
fig = plt.figure(figsize=(9, 4.8), facecolor="white")
fig.subplots_adjust(left=0.02, right=0.98, bottom=0.21, top=0.82, wspace=0.03)
start = np.array([1.0, 1.0, 0.0])
colors = ["#657789", "#196ba1", "#c14b23"]
turns = [(rotate_x, rotate_z), (rotate_z, rotate_x)]
titles = [r"First $R_x$, then $R_z$", r"First $R_z$, then $R_x$"]
lines = [r"$(1,1,0)\to(1,0,1)\to(0,1,1)$",
r"$(1,1,0)\to(-1,1,0)\to(-1,0,1)$"]
for number, (functions, title, line) in enumerate(zip(turns, titles, lines), 1):
ax = fig.add_subplot(1, 2, number, projection="3d", facecolor="white")
intermediate = functions[0](np.pi / 2, start)
end = functions[1](np.pi / 2, intermediate)
for axis, name in zip(np.eye(3), ["x", "y", "z"]):
ax.plot(*np.stack([-1.4 * axis, 1.65 * axis], axis=1),
color="#c1c8cf", lw=1.2)
ax.text(*(1.78 * axis), name, fontsize=12, color="#4d5258")
for vector, color in zip([start, intermediate, end], colors):
ax.quiver(0, 0, 0, *vector, color=color, linewidth=2.7,
arrow_length_ratio=0.13)
for function, vector, color in zip(functions, [start, intermediate], colors[1:]):
curve = np.stack([function(t, vector) for t in np.linspace(0, np.pi / 2, 100)])
ax.plot(*curve.T, color=color, lw=2, linestyle="--")
tangent = curve[-1] - curve[-8]
ax.quiver(*curve[-8], *tangent, color=color, linewidth=2,
arrow_length_ratio=0.6)
ax.set(xlim=(-1.45, 1.8), ylim=(-1.45, 1.8), zlim=(-0.5, 1.8))
ax.set_box_aspect((1, 1, 0.85))
ax.view_init(elev=23, azim=35)
ax.set_axis_off()
ax.set_title(title, fontsize=16, pad=8)
ax.text2D(0.5, -0.055, line, transform=ax.transAxes, ha="center", fontsize=12)
fig.suptitle("Order matters for rotations in three dimensions", fontsize=19)
fig.text(0.5, 0.09, "Gray: initial vector · Blue: after first turn · Orange: final vector",
ha="center", fontsize=11)
fig.text(0.5, 0.035, "Active +90° turns · right-hand rule · fixed axes · column vectors · all lengths √2",
ha="center", fontsize=11)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=150,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()