codexchordal-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()