codex/complex-polar-multiplication.py

complex-polar-multiplication.py
"""Illustrate fixed complex multiplication as a rotation and uniform scale.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Write the matching opaque PNG to the current working directory.
"""
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def arrow(ax, end, color, label, offset):
    ax.annotate("", (end.real, end.imag), (0, 0),
                arrowprops={"arrowstyle": "->", "color": color, "lw": 2.4})
    ax.annotate(label, (end.real, end.imag), xytext=offset,
                textcoords="offset points", color=color, fontsize=11,
                bbox={"facecolor": "white", "edgecolor": "none", "alpha": .9})


def main():
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    blue, orange, purple = "#2468a0", "#b65718", "#81449b"
    z, multiplier = 3+4j, 1+1j
    product = multiplier*z
    assert product == -1+7j and 1j*z == -4+3j
    assert np.isclose(abs(product), 5*np.sqrt(2))
    assert np.isclose(np.angle(product)-np.angle(z), np.pi/4)
    matrix = np.array([[1., -1.], [1., 1.]])
    assert np.allclose(matrix.T @ matrix, 2*np.eye(2))
    fig, axes = plt.subplots(1, 2, figsize=(9.4, 5.0), dpi=120,
                             constrained_layout=True)
    fig.patch.set_facecolor("white")
    left, right = axes
    arrow(left, z, blue, r"$z=3+4i$", (8, -6))
    arrow(left, 1j*z, purple, r"$iz=-4+3i$", (-22, 10))
    arrow(left, product, orange, r"$(1+i)z=-1+7i$", (-38, 9))
    arc = np.linspace(np.angle(z), np.angle(product), 150)
    left.plot(2*np.cos(arc), 2*np.sin(arc), color=orange, lw=1.8)
    left.annotate("", (2*np.cos(arc[-1]), 2*np.sin(arc[-1])),
                  (2*np.cos(arc[-8]), 2*np.sin(arc[-8])),
                  arrowprops={"arrowstyle": "->", "color": orange})
    left.text(.2, 2.4, r"$\pi/4$", color=orange)
    left.text(-5.5, -1.8, r"$|z|=|iz|=5$" + "\n" +
              r"$|(1+i)z|=5\sqrt{2}$", fontsize=11)
    left.set(title="One point: a quarter-turn or a scaled eighth-turn",
             xlim=(-6, 6), ylim=(-3.2, 8.8), xticks=[-4, -1, 0, 3],
             yticks=[0, 3, 4, 7])

    t = np.linspace(-2, 2, 100)
    for level in range(-2, 3):
        for curve in (level+1j*t, t+1j*level):
            right.plot(curve.real, curve.imag, color=blue, lw=1.0, alpha=.45)
            transformed = multiplier*curve
            right.plot(transformed.real, transformed.imag,
                       color=orange, lw=1.3, alpha=.7)
    theta = np.linspace(0, 2*np.pi, 400)
    right.plot(np.cos(theta), np.sin(theta), color=blue, lw=2)
    right.plot(np.sqrt(2)*np.cos(theta), np.sqrt(2)*np.sin(theta),
               color=orange, lw=2)
    arrow(right, 1+0j, blue, r"$1$", (0, -23))
    arrow(right, 1+1j, orange, r"$1+i$", (8, 0))
    right.plot([], [], color=blue, label="Input grid and unit circle")
    right.plot([], [], color=orange, label=r"Image under $z\mapsto(1+i)z$")
    right.legend(loc="lower center", fontsize=9, framealpha=1)
    right.set(title=r"Every point: rotation $\pi/4$, scale $\sqrt{2}$",
              xlim=(-4.8, 4.8), ylim=(-4.8, 4.8),
              xticks=[-4, -2, 0, 2, 4], yticks=[-4, -2, 0, 2, 4])
    for ax in axes:
        ax.set_aspect("equal")
        ax.set_facecolor("white")
        ax.axhline(0, color="#555555", lw=.9)
        ax.axvline(0, color="#555555", lw=.9)
        ax.set(xlabel="Real part", ylabel="Imaginary part")
        ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()