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