codex/bisection-root-brackets.py

bisection-root-brackets.py
"""Draw the first square-root bisection brackets; write the PNG to cwd.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, (graph, rows) = plt.subplots(
        2, 1, figsize=(9.2, 7.0), sharex=True,
        gridspec_kw={"height_ratios": [1.1, 1]}, layout="constrained",
    )
    fig.patch.set_facecolor("white")
    blue, red, green, gold = "#2166ac", "#b43b35", "#25834a", "#b37500"
    root = np.sqrt(2)
    x = np.linspace(0.96, 2.04, 500)
    graph.plot(x, x**2 - 2, color=blue, lw=2.5, label=r"$f(x)=x^2-2$")
    graph.axhline(0, color="0.35", lw=0.8)
    graph.axvline(root, color="0.3", ls="--", lw=1.1,
                  label=r"Root $\sqrt{2}\approx1.41421$")
    candidates = [1.5, 1.25, 1.375, 1.4375]
    for k, m in enumerate(candidates):
        value = m**2 - 2
        color = red if value > 0 else blue
        graph.scatter(m, value, color=color, s=48, zorder=5)
        offsets = [(38, 27), (-59, -21), (-77, 22), (40, -22)]
        graph.annotate(
            rf"$m_{k}={m:g}$" + (" (+)" if value > 0 else " (−)"),
            (m, value), xytext=offsets[k], textcoords="offset points",
            color=color, fontsize=10,
            arrowprops={"arrowstyle": "-", "color": color},
        )
    graph.scatter([1, 2], [-1, 2], color=green, s=45, zorder=5)
    graph.set(ylim=(-1.22, 2.28), ylabel=r"$f(x)$",
              title="Evaluate the old midpoint; keep the half with opposite signs")
    graph.legend(loc="upper left", fontsize=10, framealpha=1)

    a, b = 1.0, 2.0
    brackets = [(a, b)]
    for m in candidates:
        assert m == (a + b) / 2
        if m**2 > 2:
            b = m
        else:
            a = m
        brackets.append((a, b))
    assert brackets[-1] == (1.375, 1.4375)
    rows.axvline(root, color="0.3", ls="--", lw=1.1)
    for k, (a, b) in enumerate(brackets):
        y = 4 - k
        rows.plot([a, b], [y, y], color=green, lw=5, solid_capstyle="butt")
        rows.scatter([a, b], [y, y], color=green, marker="|", s=140, zorder=4)
        rows.text(b + 0.025, y, f"[{a:g}, {b:g}]", va="center", fontsize=10)
        if k < 4:
            m = candidates[k]
            rows.scatter(m, y, marker="D", color=gold, s=42, zorder=5)
    rows.scatter([], [], marker="D", color=gold, label="Candidate for next update")
    rows.scatter([1.40625], [0], marker="o", color="black", s=45, zorder=6,
                 label=r"Reported after 4 updates: $m_4=1.40625$")
    rows.set(xlim=(0.96, 2.28), ylim=(-1.05, 4.5), xlabel=r"$x$ (same scale in both panels)",
             yticks=[4, 3, 2, 1, 0], yticklabels=[f"k = {k}" for k in range(5)],
             title="Retained brackets after k halvings; every row still contains the root")
    rows.legend(loc="lower right", fontsize=9, framealpha=1)
    for ax in (graph, rows):
        ax.set_facecolor("white")
        ax.grid(alpha=0.15)
        ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(Path.cwd() / "bisection-root-brackets.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()