codex/chain-rule-along-a-path.py

chain-rule-along-a-path.py
"""Illustrate a field sampled along a moving curve; output a PNG to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    reference = np.pi / 4
    point = np.array([np.cos(reference), np.sin(reference)])
    velocity = np.array([-np.sin(reference), np.cos(reference)])
    gradient = np.array([2 * point[0], 4 * point[1]])
    value = point[0]**2 + 2 * point[1]**2
    rate = gradient @ velocity
    assert np.isclose(value, 1.5)
    assert np.isclose(rate, 1)
    t = np.linspace(0, np.pi / 2, 500)
    assert np.allclose(np.cos(t)**2 + 2 * np.sin(t)**2, 1 + np.sin(t)**2)
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
    fig, (spatial, sampled) = plt.subplots(1, 2, figsize=(11, 5.9), dpi=120, facecolor="white")
    grid = np.linspace(-1.35, 1.45, 400)
    x, y = np.meshgrid(grid, grid)
    contours = spatial.contour(x, y, x*x + 2*y*y, levels=[.5, 1, 1.5, 2, 3, 4],
                               colors="#aebac2", linewidths=1.2)
    spatial.clabel(contours, fmt="%g", fontsize=10)
    circle = np.linspace(0, 2 * np.pi, 600)
    spatial.plot(np.cos(circle), np.sin(circle), color="#206a9d", lw=2.8, label="Unit-circle path")
    spatial.scatter(*point, color="#202e38", s=55, zorder=5)
    for direction, scale, color in [(velocity, .58, "#b83242"), (gradient, .20, "#318468")]:
        spatial.annotate("", xy=point + scale * direction, xytext=point,
                         arrowprops={"arrowstyle": "->", "color": color, "lw": 2.6})
    spatial.text(.03, .95, r"$F(x,y)=x^2+2y^2$", transform=spatial.transAxes, va="top",
                 bbox={"facecolor": "white", "edgecolor": "none"})
    spatial.annotate("Velocity", xy=point + .58 * velocity, xytext=(-1.23, .75), color="#b83242",
                     arrowprops={"arrowstyle": "-", "color": "#b83242"}, fontsize=11)
    spatial.text(-1.18, -1.24, "Green arrow: gradient × 0.20", color="#318468", fontsize=10,
                 bbox={"facecolor": "white", "edgecolor": "none", "alpha": .9})
    spatial.text(.78, .46, r"$t=\pi/4$", fontsize=11)
    spatial.set(aspect="equal", xlim=(-1.35, 1.45), ylim=(-1.35, 1.45), xlabel="x", ylabel="y",
                title="Local field geometry and motion")
    sampled.plot(t, 1 + np.sin(t)**2, color="#206a9d", lw=3,
                 label=r"Sampled field: $1+\sin^2t$")
    tangent_domain = np.linspace(.15, 1.42, 250)
    sampled.plot(tangent_domain, value + rate * (tangent_domain - reference),
                 color="#bc7323", lw=2, linestyle="--", label="Tangent at π/4")
    sampled.scatter([reference], [value], color="#202e38", s=60, zorder=5)
    sampled.annotate("Value = 1.5\nRate = 1", xy=(reference, value), xytext=(.15, 1.86),
                     arrowprops={"arrowstyle": "->", "color": "#202e38"}, fontsize=12)
    sampled.set(xlim=(0, np.pi / 2), ylim=(.95, 2.05), xlabel="Path parameter t (radians)",
                ylabel="Field value along the path", title="Observed change along the curve")
    sampled.set_xticks([0, np.pi/4, np.pi/2], ["0", "π/4", "π/2"])
    for ax in (spatial, sampled):
        ax.grid(color="#e5e9eb")
        ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle(r"Rate along a curve = gradient · velocity", fontsize=16, y=.975)
    fig.text(.5, .10, r"At $t=\pi/4$: $(\sqrt{2},2\sqrt{2})\cdot(-1/\sqrt{2},1/\sqrt{2})=1$",
             ha="center", fontsize=14)
    fig.legend(*sampled.get_legend_handles_labels(), loc="lower center", ncol=2,
               frameon=False, bbox_to_anchor=(.5, .018), fontsize=11)
    fig.subplots_adjust(left=.07, right=.98, top=.84, bottom=.23, wspace=.30)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()