codex/chordal-perfect-elimination-triangle-chain.py

chordal-perfect-elimination-triangle-chain.py
"""Exact original graph and elimination trace; write PNG basename to CWD.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Dashed edges denote symbolic fill, not observations or numerical measurements.
"""
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Polygon


OUTPUT = Path(__file__).with_suffix('.png').name
EDGES = [('a', 'b'), ('a', 'c'), ('b', 'c'), ('b', 'd'),
         ('c', 'd'), ('c', 'e'), ('d', 'e')]
FILL = [('a', 'd'), ('a', 'e'), ('b', 'e')]
POS = {'a': (0, 0), 'b': (0.9, 1), 'c': (1.8, 0),
       'd': (2.7, 1), 'e': (3.6, 0)}
INK, BLUE, TEAL, RED = '#243447', '#2563a0', '#087e82', '#b43c38'


def draw(ax, bad=False):
    if not bad:
        for clique, color in [(('a', 'b', 'c'), '#e4f1fb'),
                               (('b', 'c', 'd'), '#e8f4ee'),
                               (('c', 'd', 'e'), '#fff1da')]:
            ax.add_patch(Polygon([POS[v] for v in clique], facecolor=color,
                                 edgecolor='none', zorder=0))
    for u, v in EDGES:
        if bad and 'c' in (u, v):
            continue
        ax.plot(*zip(POS[u], POS[v]), color=INK, lw=2.2, zorder=1)
    if bad:
        for u, v in FILL:
            ax.plot(*zip(POS[u], POS[v]), color=RED, lw=2.4,
                    ls=(0, (4, 3)), zorder=2)
    for v, (x, y) in POS.items():
        if bad and v == 'c':
            ax.text(x, y - 0.12, 'c removed', fontsize=11, ha='center',
                    color='#6e7a87')
            continue
        ax.scatter(x, y, s=620, color='white', edgecolor=BLUE, lw=2, zorder=3)
        ax.text(x, y, v, fontsize=16, color=INK, ha='center', va='center', zorder=4)
    ax.set(xlim=(-0.3, 3.9), ylim=(-0.45, 1.42), aspect='equal')
    ax.axis('off')


def main():
    plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 12})
    fig = plt.figure(figsize=(11, 6.25), dpi=110, facecolor='white')
    grid = fig.add_gridspec(2, 2, height_ratios=[1.02, 1.15],
                           left=0.04, right=0.97, top=0.84, bottom=0.11,
                           wspace=0.15, hspace=0.23)
    fig.suptitle('The same chordal graph, two first-pivot choices',
                 fontsize=19, color=INK, y=0.96)
    a = fig.add_subplot(grid[0, 0])
    b = fig.add_subplot(grid[0, 1])
    draw(a)
    draw(b, bad=True)
    a.set_title('Three triangles: start with a', fontsize=14, color=INK)
    b.set_title('Start with c: three fill edges', fontsize=14, color=INK)
    a.text(1.8, -0.38, 'Neighbors {b, c} already form a clique.', ha='center', fontsize=12)
    b.text(1.8, -0.38, 'Dashed new edges: ad, ae, be', ha='center', fontsize=12, color=RED)
    trace = fig.add_subplot(grid[1, 0])
    trace.axis('off')
    trace.set_title('Perfect deletion order: a, b, c, d, e', fontsize=13, color=INK, pad=8)
    table = trace.table(cellText=[['a', '{b, c}', 'bc'], ['b', '{c, d}', 'cd'],
                                  ['c', '{d, e}', 'de'], ['d', '{e}', 'none needed'],
                                  ['e', '{}', 'none needed']],
                        colLabels=['Delete', 'Later neighbors', 'Existing edge'],
                        cellLoc='center', colWidths=[0.17, 0.43, 0.40],
                        bbox=[0, 0.02, 1, 0.91])
    table.auto_set_font_size(False)
    table.set_fontsize(12)
    for (row, col), cell in table.get_celld().items():
        cell.set_edgecolor('#c9d4df')
        cell.set_facecolor('#edf6f8' if row == 0 else 'white')
        cell.get_text().set_color(INK)
    explain = fig.add_subplot(grid[1, 1])
    explain.axis('off')
    explain.text(0.5, 0.92, 'Clique tree: shared variables stay connected', ha='center',
                 fontsize=13, color=INK)
    labels = ['{a, b, c}', '{b, c, d}', '{c, d, e}']
    for x, label in zip([0.14, 0.50, 0.86], labels):
        explain.text(x, 0.64, label, ha='center', va='center', fontsize=12,
                     bbox={'boxstyle': 'round,pad=0.35', 'facecolor': '#edf6f8',
                           'edgecolor': TEAL, 'linewidth': 1.5})
    for x, label in [(0.32, '{b, c}'), (0.68, '{c, d}')]:
        explain.plot([x - 0.085, x + 0.085], [0.64, 0.64], color=TEAL, lw=2)
        explain.text(x, 0.44, label, ha='center', fontsize=12, color=TEAL)
    explain.text(0.5, 0.22, 'Maximum cardinality search selects e, d, c, b, a.\n'
                 'Reverse this sequence for deletion.\nNo symbolic fill with the perfect order.',
                 ha='center', va='center', fontsize=12, color=INK, linespacing=1.6)
    explain.set(xlim=(0, 1), ylim=(0, 1))
    fig.text(0.5, 0.035, 'Exact finite graphs • fill edges show the structural elimination rule',
             ha='center', fontsize=11, color='#536274')
    fig.savefig(OUTPUT, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()