codex/lu-pivot-repeated-solves.py

lu-pivot-repeated-solves.py
"""Original zero-pivot and reusable triangular-solve diagram.

Writes its matching PNG in the CWD. Tested with Python 3.14,
Matplotlib 3.10.7 and NumPy 2.3.5. Run from _media with Agg.
"""
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, FancyBboxPatch
import numpy as np


def matrix(ax, x, y, values, label, highlight=None):
    cell = 0.53
    ax.text(x+cell,y+0.35,label,ha='center',fontsize=14)
    for i,row in enumerate(values):
        for j,value in enumerate(row):
            color = '#ffe2a1' if (i,j)==highlight else '#f2f6fa'
            ax.add_patch(Rectangle((x+j*cell,y-(i+1)*cell),cell,cell,
                                   facecolor=color,edgecolor='#b9c4cf'))
            ax.text(x+(j+0.5)*cell,y-(i+0.5)*cell,str(value),
                    ha='center',va='center',fontsize=16,color='#172c40')


def box(ax, x, y, w, h, text, color):
    ax.add_patch(FancyBboxPatch((x,y),w,h,boxstyle='round,pad=0.04',
                               facecolor=color,edgecolor='#7d98ab',lw=1.1))
    ax.text(x+w/2,y+h/2,text,ha='center',va='center',fontsize=12)


def main():
    plt.rcParams.update({'font.family':'DejaVu Sans','font.size':12})
    a=np.array([[0,1],[2,3]]); p=np.array([[0,1],[1,0]])
    l=np.eye(2,dtype=int); u=np.array([[2,3],[0,1]])
    b=np.array([[1,-1,1],[5,-3,7]]); x=np.array([[1,0,2],[1,-1,1]])
    assert np.array_equal(p @ a,l @ u)
    assert np.array_equal(a @ x,b)
    fig,ax=plt.subplots(figsize=(9.4,6.3),facecolor='white')
    fig.subplots_adjust(left=0.015,right=0.985,bottom=0.025,top=0.97)
    ax.set(xlim=(0,9.4),ylim=(0,6.3)); ax.axis('off')
    ax.text(4.7,6.0,'A row swap fixes a zero pivot; reuse the factors for every b',
            ha='center',fontsize=16,fontweight='bold',color='#172c40')
    matrix(ax,0.6,5.2,a,'$A_0$',highlight=(0,0))
    ax.text(1.13,3.9,'First pivot is zero,\nbut det A₀ = −2.',ha='center',va='top',fontsize=11,color='#6d4908')
    ax.annotate('',xy=(4.0,4.65),xytext=(1.9,4.65),arrowprops={'arrowstyle':'->','lw':2,'color':'#31678d'})
    ax.text(3.0,5.05,'Swap rows 1 and 2',ha='center',fontsize=12)
    ax.text(3.0,4.2,'$P$ records the swap',ha='center',fontsize=11)
    matrix(ax,4.2,5.2,u,'$PA_0=U$',highlight=(0,0))
    matrix(ax,6.15,5.2,l,'$L=I$')
    ax.text(8.0,4.65,'$PA_0=LU$\n\nFactor once',ha='center',va='center',fontsize=13)
    ax.plot([0.4,9.0],[3.45,3.45],color='#d8dee5')
    ax.text(4.7,3.16,'Use the same P, L and U on each row below',ha='center',fontsize=13,color='#31678d')
    box(ax,0.45,2.48,1.4,0.4,'Input b','#ffffff')
    box(ax,2.4,2.48,1.4,0.4,'Permute: Pb','#eef4fa')
    box(ax,4.38,2.48,1.78,0.4,'Forward: Ly = Pb','#e7f2ed')
    box(ax,6.77,2.48,2.05,0.4,'Back: Ux = y','#eef4fa')
    for x0,x1 in [(1.92,2.31),(3.88,4.29),(6.24,6.67)]:
        ax.annotate('',xy=(x1,2.68),xytext=(x0,2.68),arrowprops={'arrowstyle':'->','lw':1.6,'color':'#31678d'})
    for j,y in enumerate([2.03,1.46,0.89]):
        texts=[f'({b[0,j]}, {b[1,j]})ᵀ',f'({b[1,j]}, {b[0,j]})ᵀ',
               f'({b[1,j]}, {b[0,j]})ᵀ',f'({x[0,j]}, {x[1,j]})ᵀ']
        for mid,t in zip([1.15,3.1,5.27,7.8],texts):
            ax.text(mid,y,t,ha='center',va='center',fontsize=14)
        ax.plot([0.45,8.82],[y-0.26,y-0.26],color='#e1e7ed',lw=0.8)
    ax.text(4.7,0.27,'Here L = I, so y = Pb. In general, forward substitution also does arithmetic.',
            ha='center',fontsize=11,color='#52606d')
    fig.savefig(Path(__file__).with_suffix('.png').name,dpi=120,facecolor='white',transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()