codex/continuation-pseudo-arclength-predictor-corrector.py

continuation-pseudo-arclength-predictor-corrector.py
"""Draw one pseudo-arclength step across a cubic solution curve's fold.

The tangent is normalized in the displayed Euclidean coordinates.
Run from the output directory; dependency versions are in pyproject.toml.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def continuation_step(x0=0.45, h=0.18):
    seed = np.array([x0, x0 - x0**3])
    tangent = np.array([1.0, 1.0 - 3 * x0**2])
    tangent /= np.linalg.norm(tangent)
    predictor = seed + h * tangent
    corrected = predictor.copy()
    for _ in range(12):
        x, lam = corrected
        residual = np.array([x**3 - x + lam, tangent @ (corrected - predictor)])
        if np.linalg.norm(residual, ord=np.inf) < 2e-14:
            break
        jacobian = np.array([[3 * x**2 - 1, 1.0], tangent])
        corrected += np.linalg.solve(jacobian, -residual)
    x, lam = corrected
    assert abs(x**3 - x + lam) < 2e-14
    assert abs(tangent @ (corrected - seed) - h) < 2e-14
    assert x0 < 1 / np.sqrt(3) < x
    return seed, tangent, predictor, corrected


def main():
    seed, tangent, predictor, corrected = continuation_step()
    print({"seed": seed.tolist(), "tangent": tangent.tolist(),
           "predictor": predictor.tolist(), "corrected": corrected.tolist()})
    normal = np.array([-tangent[1], tangent[0]])
    line = predictor + np.linspace(-0.11, 0.055, 100)[:, None] * normal
    x = np.linspace(0.32, 0.80, 400)
    fold = np.array([1 / np.sqrt(3), 2 / (3 * np.sqrt(3))])
    fig, ax = plt.subplots(figsize=(8.2, 5.0), dpi=120, layout="constrained")
    fig.set_facecolor("white")
    ax.set_facecolor("white")
    ax.plot(x, x - x**3, color="#176b8e", lw=2.5, label=r"Solution curve $F=0$")
    ax.plot(line[:, 0], line[:, 1], color="#7c3aed", ls="--", lw=1.5,
            label="Transverse line through predictor")
    ax.annotate("", xy=predictor, xytext=seed,
                arrowprops={"arrowstyle": "->", "color": "#64748b", "lw": 2})
    ax.annotate("", xy=corrected, xytext=predictor,
                arrowprops={"arrowstyle": "->", "color": "#7c3aed", "lw": 2})
    ax.scatter(*fold, color="#b45309", s=55, zorder=5)
    ax.scatter(*seed, color="#176b8e", s=45, zorder=5)
    ax.scatter(*predictor, color="#64748b", marker="s", s=45, zorder=5)
    ax.scatter(*corrected, color="#7c3aed", s=45, zorder=5)
    for point, text, offset in (
        (seed, r"Seed $z_k$", (-40, -27)),
        (predictor, r"Predictor $z_{\mathrm{p}}$", (-4, 16)),
        (corrected, r"Corrected $z_{k+1}$", (14, -23)),
        (fold, "Fold", (-34, 20)),
    ):
        ax.annotate(text, xy=point, xytext=offset, textcoords="offset points", fontsize=11)
    # A small right-angle marker shows the two perpendicular directions.
    size = 0.012
    corner = np.array([predictor - size * tangent,
                       predictor - size * tangent - size * normal,
                       predictor - size * normal])
    ax.plot(corner[:, 0], corner[:, 1], color="#475569", lw=1)
    ax.set_aspect("equal", adjustable="box")
    ax.set(xlabel=r"State $x$", ylabel=r"Parameter $\lambda$",
           xlim=(0.32, 0.80), ylim=(0.285, 0.49),
           title=r"Predict along the tangent; correct on a transverse line ($h=0.18$)")
    ax.legend(loc="upper left", fontsize=9, framealpha=1)
    ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()