codex/gradient-directional-derivative.py

gradient-directional-derivative.py
"""Exact directional derivatives of x^2 + 2 y^2 at (1, 1)."""

from pathlib import Path

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


def main():
    point = np.array([1.0, 1.0])
    gradient = np.array([2.0, 4.0])
    best = gradient / np.linalg.norm(gradient)
    tangent = np.array([-2.0, 1.0]) / np.sqrt(5)
    assert np.isclose(gradient @ best, 2 * np.sqrt(5))
    assert np.isclose(gradient @ tangent, 0)
    angle = np.linspace(0, 2 * np.pi, 1001)
    rates = 2 * np.cos(angle) + 4 * np.sin(angle)
    grid = np.linspace(-2, 2, 401)
    x, y = np.meshgrid(grid, grid)
    fig, axes = plt.subplots(1, 2, figsize=(10.5, 5.1), dpi=120)
    fig.patch.set_facecolor("white")
    ax = axes[0]
    lines = ax.contour(x, y, x**2 + 2 * y**2,
                      levels=[0.5, 1, 2, 3, 4, 6, 8],
                      colors="#b7c2cb", linewidths=1)
    ax.clabel(lines, fontsize=9, inline=True)
    ax.contour(x, y, x**2 + 2 * y**2, levels=[3],
               colors="#333e60", linewidths=2)
    ax.scatter(*point, color="#222222", s=35, zorder=4)
    for direction, color in [(best, "#c76716"), (tangent, "#1675a9")]:
        ax.annotate("", xy=point + 0.65 * direction, xytext=point,
                    arrowprops={"arrowstyle": "-|>", "color": color,
                                "lw": 2.5, "mutation_scale": 18})
    ax.text(1.2, 1.65, "steepest", color="#c76716", fontsize=11)
    ax.text(0.22, 1.44, "tangent", color="#1675a9", fontsize=11)
    ax.text(1.1, 0.85, "(1, 1)", fontsize=10)
    ax.set_aspect("equal")
    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_title(r"Level curves of $f=x^2+2y^2$", fontsize=13)
    ax.text(0.5, -0.23, "Arrows show unit directions at the marked point.",
            transform=ax.transAxes, ha="center", fontsize=10)
    ax = axes[1]
    ax.plot(np.degrees(angle), rates, color="#333e60", lw=2)
    ax.axhline(0, color="#999999", lw=0.8)
    theta_best = np.arctan2(4, 2)
    theta_tangent = theta_best + np.pi / 2
    ax.scatter(np.degrees(theta_best), 2 * np.sqrt(5),
               color="#c76716", s=45, zorder=4)
    ax.scatter(np.degrees(theta_tangent), 0,
               color="#1675a9", s=45, zorder=4)
    ax.annotate(r"maximum $2\sqrt{5}$", xy=(np.degrees(theta_best), 2 * np.sqrt(5)),
                xytext=(115, 4.6), fontsize=11, color="#c76716",
                arrowprops={"arrowstyle": "-", "color": "#c76716"})
    ax.annotate("tangent: rate 0", xy=(np.degrees(theta_tangent), 0),
                xytext=(190, 1.5), fontsize=11, color="#1675a9",
                arrowprops={"arrowstyle": "-", "color": "#1675a9"})
    ax.set_xlim(0, 360)
    ax.set_ylim(-5.2, 5.5)
    ax.set_xticks([0, 90, 180, 270, 360])
    ax.set_xlabel("Unit direction angle (degrees)")
    ax.set_ylabel("Change per unit distance")
    ax.set_title(r"$D_{(\cos\theta,\sin\theta)}f=2\cos\theta+4\sin\theta$",
                 fontsize=12)
    fig.suptitle("One gradient predicts the rate in every direction",
                 fontsize=16, y=0.98)
    fig.subplots_adjust(left=0.07, right=0.97, bottom=0.22, top=0.82, wspace=0.35)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()