codex/midpoint-trapezoidal-error-refinement.py

midpoint-trapezoidal-error-refinement.py
"""Plot exact quadratic quadrature errors and a controlled sample-bias model.

Run from _media. Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    panels = 2 ** np.arange(8)
    midpoint_errors, trapezoid_errors, biased_errors = [], [], []
    bias = 0.002
    for n in panels:
        x = np.linspace(0, 1, n + 1)
        midpoints = (x[:-1] + x[1:]) / 2
        m = np.sum(midpoints**2) / n
        t = (np.sum(x[1:-1]**2) + (x[0]**2 + x[-1]**2) / 2) / n
        observed = x**2 + bias
        biased_t = (np.sum(observed[1:-1]) + (observed[0] + observed[-1]) / 2) / n
        midpoint_errors.append(1 / 3 - m)
        trapezoid_errors.append(t - 1 / 3)
        biased_errors.append(biased_t - 1 / 3)
    midpoint_errors = np.asarray(midpoint_errors)
    trapezoid_errors = np.asarray(trapezoid_errors)
    biased_errors = np.asarray(biased_errors)
    np.testing.assert_allclose(midpoint_errors, 1 / (12 * panels**2), rtol=1e-10)
    np.testing.assert_allclose(trapezoid_errors, 1 / (6 * panels**2), rtol=1e-10)
    np.testing.assert_allclose(biased_errors, trapezoid_errors + bias, rtol=1e-10)
    fig, axes = plt.subplots(1, 2, figsize=(10.4, 4.4), layout="constrained")
    fig.set_facecolor("white")
    for ax in axes:
        ax.set_facecolor("white")
        ax.set_xscale("log", base=2)
        ax.set_yscale("log")
        ax.set_xticks(panels, labels=[str(n) for n in panels])
        ax.set(xlabel="Number of panels $n$", ylabel="Absolute error against $I=1/3$")
        ax.grid(which="major", color="#e4e7eb")
        ax.spines[["top", "right"]].set_visible(False)
    axes[0].plot(panels, trapezoid_errors, "o-", color="#b75a16", label="$T_n-I=1/(6n^2)$")
    axes[0].plot(panels, midpoint_errors, "s-", color="#236aa0", label="$I-M_n=1/(12n^2)$")
    axes[0].set_title("Exact samples: errors fall by four\nwhen the panel count doubles", fontsize=12)
    axes[1].plot(panels, biased_errors, "o-", color="#b75a16", label="Samples $x_i^2+0.002$")
    axes[1].plot(panels, trapezoid_errors, "--", color="#757b83", label="Exact samples")
    axes[1].axhline(bias, color="#7b3e8c", lw=1.5, label="Common bias $\\delta=0.002$")
    axes[1].set_title("Biased samples: refinement leaves a floor\n$T_n^{\\mathrm{obs}}-I=1/(6n^2)+\\delta$", fontsize=12)
    for ax in axes:
        ax.legend(loc="lower left", fontsize=9, facecolor="white", framealpha=1)
    fig.suptitle("Composite quadrature for $f(x)=x^2$ on $[0,1]$", fontsize=15)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()