codex/quadrature-sample-weights-and-panels.py

quadrature-sample-weights-and-panels.py
"""Compare quadrature panels and the weights of their actual samples.

Run from _media; outputs quadrature-sample-weights-and-panels.png to CWD.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    fig, axes = plt.subplots(2, 2, figsize=(9.6, 6.8), layout="constrained")
    fig.set_facecolor("white")
    curve = np.linspace(0, 1, 501)
    for row, n in enumerate((1, 2)):
        edges = np.linspace(0, 1, n + 1)
        for col, method in enumerate(("Midpoint", "Trapezoidal")):
            ax = axes[row, col]
            color = "#2166a3" if col == 0 else "#b65716"
            ax.set_facecolor("white")
            if col == 0:
                nodes = (edges[:-1] + edges[1:]) / 2
                weights = np.full(n, 1 / n)
                for left, right, node in zip(edges[:-1], edges[1:], nodes):
                    ax.fill_between([left, right], node**2, color=color, alpha=0.16)
                    ax.plot([left, left, right, right], [0, node**2, node**2, 0], color=color)
                value = np.dot(weights, nodes**2)
                np.testing.assert_allclose(value, 1 / 3 - 1 / (12 * n**2))
                answer = "1/4" if n == 1 else "5/16"
                weight_labels = ["1" if n == 1 else "1/2"] * n
            else:
                nodes = edges
                weights = np.full(n + 1, 1 / n)
                weights[[0, -1]] /= 2
                ax.fill_between(nodes, nodes**2, color=color, alpha=0.16)
                ax.plot(nodes, nodes**2, color=color)
                for node in nodes:
                    ax.plot([node, node], [0, node**2], color=color, lw=1)
                value = np.dot(weights, nodes**2)
                np.testing.assert_allclose(value, 1 / 3 + 1 / (6 * n**2))
                answer = "1/2" if n == 1 else "3/8"
                weight_labels = ["1/2", "1/2"] if n == 1 else ["1/4", "1/2", "1/4"]
            np.testing.assert_allclose(weights.sum(), 1)
            ax.plot(curve, curve**2, color="#252b32", lw=2, label=r"$f(x)=x^2$")
            ax.scatter(nodes, nodes**2, s=38, color=color, zorder=4, clip_on=False)
            ax.axhline(0, color="#49525c", lw=0.8)
            # A separate strip records nodes and weights, not extra function values.
            ax.axhline(-0.18, color="#b9c0c7", lw=0.8)
            ax.scatter(nodes, np.full_like(nodes, -0.18), color=color, s=25, clip_on=False)
            for node, label in zip(nodes, weight_labels):
                ax.text(node, -0.27, "$" + label + "$", ha="center", va="center", fontsize=11)
            symbol = "M" if col == 0 else "T"
            ax.set_title(f"{method}, {n} panel{'s' if n > 1 else ''}: ${symbol}_{n}={answer}$", fontsize=12)
            ax.set(xlim=(-0.06, 1.06), ylim=(-0.34, 1.09), xlabel="$x$", ylabel="Height")
            ax.set_xticks([0, 0.25, 0.5, 0.75, 1], ["0", "1/4", "1/2", "3/4", "1"])
            ax.set_yticks([0, 0.5, 1])
            ax.spines[["top", "right", "bottom"]].set_visible(False)
            ax.grid(axis="y", color="#e3e6e9", zorder=0)
            if row == 0 and col == 0:
                ax.legend(loc="upper left", fontsize=10, framealpha=1)
    fig.suptitle(r"Same exact integral $I=1/3$, different samples and weights", fontsize=15)
    fig.supxlabel("Dots below each graph show sample locations; fractions give quadrature weights.", fontsize=11)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()