codex/polynomial-roots-and-multiplicity.py

polynomial-roots-and-multiplicity.py
"""Exact cubic roots, crossing parity, and a repeated-root perturbation."""

from pathlib import Path

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


def main():
    x = np.linspace(-2, 3.5, 1001)
    p = lambda t: (t + 1) * (t - 2) ** 2
    assert p(-1) == 0 and p(2) == 0 and p(3) == 4
    assert p(-1.01) < 0 < p(-0.99)
    assert p(1.99) > 0 and p(2.01) > 0
    assert np.allclose(p(x), x**3 - 3*x**2 + 4)
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.2), dpi=120)
    blue, orange, green = "#176997", "#b55d20", "#43804d"
    ax = axes[0]
    ax.plot(x, p(x), color=blue, lw=2)
    ax.axhline(0, color="#74818c", lw=0.9)
    ax.axvline(0, color="#aeb8c0", lw=0.7)
    ax.scatter([-1, 2], [0, 0], color=[green, orange], s=50, zorder=4)
    ax.annotate("simple root: crosses", xy=(-1, 0), xytext=(-1.8, 8),
                fontsize=11, color=green,
                arrowprops={"arrowstyle": "->", "color": green})
    ax.annotate("double root: touches", xy=(2, 0), xytext=(0.5, -8),
                fontsize=11, color=orange,
                arrowprops={"arrowstyle": "->", "color": orange})
    ax.set_xlim(-2, 3.5)
    ax.set_ylim(-18, 12)
    ax.set_xticks([-2, -1, 0, 1, 2, 3])
    ax.set_xlabel("Real input x")
    ax.set_ylabel("Polynomial value")
    ax.set_title(r"$p(x)=(x+1)(x-2)^2=x^3-3x^2+4$", fontsize=13)
    ax = axes[1]
    ax.axhline(0, color="#74818c", lw=1)
    ax.axvline(2, color="#d0d7dd", lw=0.8, ls=":")
    roots_real = [1.8, 2.2]
    roots_complex = [2 + 0.2j, 2 - 0.2j]
    for z in roots_real:
        assert abs((z - 2) ** 2 - 0.04) < 1e-12
    for z in roots_complex:
        assert abs((z - 2) ** 2 + 0.04) < 1e-12
    ax.scatter(roots_real, [0, 0], color=green, s=65, zorder=4)
    ax.scatter([2, 2], [0.2, -0.2], color=blue, s=65, zorder=4)
    ax.scatter([2], [0], facecolor="white", edgecolor=orange, s=140, lw=2, zorder=5)
    for start, end, color in [((2, 0), (1.8, 0), green), ((2, 0), (2.2, 0), green),
                              ((2, 0), (2, 0.2), blue), ((2, 0), (2, -0.2), blue)]:
        ax.annotate("", xy=end, xytext=start,
                    arrowprops={"arrowstyle": "->", "color": color,
                                "shrinkA": 10, "shrinkB": 8, "lw": 1.5})
    ax.text(1.8, -0.055, "1.8", ha="center", color=green, fontsize=11)
    ax.text(2.2, -0.055, "2.2", ha="center", color=green, fontsize=11)
    ax.text(2.025, 0.2, "2 + 0.2i", va="center", color=blue, fontsize=11)
    ax.text(2.025, -0.2, "2 − 0.2i", va="center", color=blue, fontsize=11)
    ax.text(1.62, 0.30, r"$\varepsilon=+0.04$: two real roots", color=green, fontsize=10)
    ax.text(1.62, -0.31, r"$\varepsilon=-0.04$: two complex roots", color=blue, fontsize=10)
    ax.set_xlim(1.58, 2.45)
    ax.set_ylim(-0.36, 0.36)
    ax.set_aspect("equal")
    ax.set_xlabel("Real part of root")
    ax.set_ylabel("Imaginary part")
    ax.set_title(r"Roots of $(x-2)^2-\varepsilon$", fontsize=13)
    fig.suptitle("A repeated factor changes both graph shape and root sensitivity",
                 fontsize=15, y=0.97)
    fig.subplots_adjust(left=0.07, right=0.975, top=0.81, bottom=0.15, wspace=0.30)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()