codex/dot-product-angle-sign.py

dot-product-angle-sign.py
"""Draw dot-product signs for fixed lengths; write an opaque PNG to the cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    a = np.array([3.0, 4.0])
    angles = np.deg2rad([0, 60, 90, 120, 180])
    alpha = np.arctan2(a[1], a[0])
    vectors = 2 * np.column_stack((np.cos(alpha + angles), np.sin(alpha + angles)))
    products = vectors @ a
    expected = np.array([10, 5, 0, -5, -10])
    np.testing.assert_allclose(products, expected, atol=1e-13)
    np.testing.assert_allclose(np.linalg.norm(vectors, axis=1), 2)
    np.testing.assert_allclose(products / (np.linalg.norm(a) * 2), np.cos(angles), atol=1e-14)
    for direction in [np.array([2.0, 0.0]), np.array([-2.0, 0.0])]:
        projection = np.dot(a, direction) / np.dot(direction, direction) * direction
        np.testing.assert_allclose(projection, [3, 0])
        np.testing.assert_allclose(np.dot(a - projection, direction), 0)
    np.testing.assert_allclose(np.dot(a, [4, -3]), 0)
    np.testing.assert_allclose(np.linalg.norm([4, -3]), 5)
    np.testing.assert_allclose(np.dot(a, [-2, 0]), -6)
    np.testing.assert_allclose(np.dot(a, [-1, 0]), -3)
    np.testing.assert_allclose([np.dot(a, [2, 0]) / 10, np.dot(a, [-2, 0]) / 10], [.6, -.6])
    np.testing.assert_allclose(np.degrees(np.arccos(-.6)), 126.86989764584402)
    np.testing.assert_allclose(np.dot([1, 1], [1, -4]), -3)
    np.testing.assert_allclose(np.dot(np.array([4, 1]) * [1, 1], [1, -4]), 0)

    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, (curve, rays) = plt.subplots(1, 2, figsize=(10.2, 5.1),
                                    gridspec_kw={"width_ratios": [1.2, 1]}, facecolor="white")
    fig.subplots_adjust(left=.08, right=.97, bottom=.19, top=.78, wspace=.32)
    theta = np.linspace(0, np.pi, 501)
    curve.plot(theta, 10 * np.cos(theta), color="#245b96", lw=2.3)
    curve.axhline(0, color="#888888", lw=.9)
    curve.axvline(np.pi / 2, color="#999999", ls="--", lw=1)
    colors = ["#287446", "#287446", "#666666", "#b34e12", "#b34e12"]
    for t, value, color in zip(angles, expected, colors):
        curve.plot(t, value, "o", color=color, ms=7)
        label = f"{int(value):+d}" if value != 0 else "0"
        curve.annotate(label, xy=(t, value), xytext=(0, 12 if value >= 0 else -20),
                       textcoords="offset points", ha="center", color=color, weight="bold")
    curve.set_xlim(-.12, np.pi + .12)
    curve.set_ylim(-12.5, 13)
    curve.set_xticks(angles, [r"$0^\circ$", r"$60^\circ$", r"$90^\circ$", r"$120^\circ$", r"$180^\circ$"])
    curve.set_yticks([-10, -5, 0, 5, 10])
    curve.set_xlabel(r"Angle $\theta$ between $a$ and $b$")
    curve.set_ylabel(r"Dot product $a\cdot b$")
    curve.set_title(r"$a\cdot b=10\cos\theta$", fontsize=13, pad=10)
    curve.grid(color="#e7e7e7", lw=.7)

    rays.set_aspect("equal")
    rays.set_xlim(-3.8, 4.2)
    rays.set_ylim(-3.2, 5.1)
    rays.axhline(0, color="#cccccc", lw=.8)
    rays.axvline(0, color="#cccccc", lw=.8)
    circle = np.linspace(0, 2 * np.pi, 361)
    rays.plot(2 * np.cos(circle), 2 * np.sin(circle), color="#cccccc", ls="--", lw=1)
    rays.annotate("", xy=a, xytext=(0, 0),
                  arrowprops=dict(arrowstyle="-|>", color="#245b96", lw=2.1, mutation_scale=16))
    rays.text(2.6, 4.28, r"$a=(3,4)$", color="#245b96", fontsize=11)
    for index, (vector, color, degree, value) in enumerate(zip(vectors, colors, [0, 60, 90, 120, 180], expected)):
        rays.annotate("", xy=vector, xytext=(0, 0),
                      arrowprops=dict(arrowstyle="-|>", color=color, lw=2.8, mutation_scale=14))
        point = vector * ([1.3, 1.3, 1.8, 1.3, 1.3][index])
        label = f"{degree}°\ndot {int(value):+d}" if value != 0 else f"{degree}°\ndot 0"
        rays.text(*point, label, ha="center", va="center", color=color, fontsize=10,
                  bbox=dict(facecolor="white", edgecolor="none", pad=1))
    rays.text(.16, -.3, "O", fontsize=10)
    rays.set_xticks([])
    rays.set_yticks([])
    for spine in rays.spines.values():
        spine.set_visible(False)
    rays.set_title(r"Same $a$; every $b$ has length $2$", fontsize=12, pad=10)
    fig.suptitle("Alignment changes the sign of the dot product", y=.96, fontsize=17)
    fig.text(.5, .86, r"$\|a\|=5,\quad \|b\|=2,\quad 0\leq\theta\leq\pi$", ha="center", fontsize=13)
    fig.text(.5, .07, "Green: aligned component    Grey: perpendicular    Orange: opposing component",
             ha="center", fontsize=11)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=140,
                facecolor="white", transparent=False)
    plt.close(fig)
    print("Verified dot products [10, 5, 0, -5, -10], all b lengths 2, cosine ratios, "
          "both projection directions, perpendicular pair, and zero residual dot products.")


if __name__ == "__main__":
    main()