83e9dba8ac
- Move the iterated-reduction algorithm, its two structural lemmas
(exactly-one-match, all-distinct-exists), and the n=14 trace figure
into a new companion paper at
papers/dual_decomposition_iterated_reduction/. Figures and figure
scripts moved via git mv (history preserved).
- In the main paper, Section 3 ("An iterated reduction") becomes
Section 3 "Cubic-graph edge contraction" (just the contraction
definition + 4-face theorem).
- Restructure Section 4 to host both the original face-monochromatic-pair
conjecture (clauses 1-3) and its strengthening (adds clause 4) as
separate conjectures, after briefly experimenting with folding them
into one. The empirical evidence is asymmetric (n<=21 for (1)-(3),
n<=18 for the full set), which the two-conjecture split presents more
honestly. The companion-paper reference is now in Section 4's intro.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
220 lines
7.6 KiB
Python
220 lines
7.6 KiB
Python
"""Draw the iterated reduction algorithm's trace on the dodecahedron.
|
|
|
|
Produces three figures:
|
|
fig_alg_step0.png -- G' (dodecahedron) with F_v (inner pentagon) shaded.
|
|
fig_alg_step1.png -- H_1 (post step 1), 3-edge-coloured; 4 protected edges.
|
|
fig_alg_step2.png -- H_2 (post step 2), 3-edge-coloured; 8 protected edges;
|
|
algorithm terminates.
|
|
|
|
Run with: sage experiments/draw_iterated_reduction.py
|
|
"""
|
|
from sage.all import Graph
|
|
from sage.graphs.graph_coloring import edge_coloring
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.patches import Polygon
|
|
import math
|
|
import os
|
|
|
|
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
C = ['#dc2626', '#16a34a', '#2563eb'] # proper-edge-colour palette
|
|
GRAY = '#9ca3af'
|
|
DARK = '#374151'
|
|
HIGHLIGHT = '#fef3c7'
|
|
|
|
|
|
def dodecahedron_positions():
|
|
pos = {}
|
|
R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8}
|
|
for i in range(5):
|
|
for fam in ('a', 'b'):
|
|
th = math.radians(90 - 72 * i)
|
|
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
|
for fam in ('c', 'd'):
|
|
th = math.radians(90 - 72 * i - 36)
|
|
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
|
return pos
|
|
|
|
|
|
def build_dodecahedron():
|
|
edges = []
|
|
for i in range(5):
|
|
edges.append((('a', i), ('a', (i + 1) % 5)))
|
|
edges.append((('a', i), ('b', i)))
|
|
edges.append((('b', i), ('c', i)))
|
|
edges.append((('b', i), ('c', (i - 1) % 5)))
|
|
edges.append((('c', i), ('d', i)))
|
|
edges.append((('d', i), ('d', (i + 1) % 5)))
|
|
G = Graph(edges, multiedges=False, loops=False)
|
|
G.is_planar(set_embedding=True)
|
|
return G
|
|
|
|
|
|
def find_safe_pentagonal_face(G, protected):
|
|
for face in G.faces():
|
|
if len(face) != 5:
|
|
continue
|
|
boundary = [u for (u, v) in face]
|
|
boundary_edges = [frozenset([u, v]) for (u, v) in face]
|
|
externals = []
|
|
A = []
|
|
for B_k in boundary:
|
|
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
|
if len(outer) != 1:
|
|
break
|
|
externals.append(frozenset([B_k, outer[0]]))
|
|
A.append(outer[0])
|
|
else:
|
|
if not any(e in protected for e in boundary_edges + externals):
|
|
return boundary, externals, A
|
|
return None
|
|
|
|
|
|
def valid_indices(f_vec):
|
|
out = []
|
|
for i in range(5):
|
|
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
|
|
continue
|
|
if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) == 3:
|
|
out.append(i)
|
|
return out
|
|
|
|
|
|
def draw(ax, G, pos, *, coloring=None, protected=None,
|
|
shade_face=None):
|
|
if shade_face:
|
|
poly = [pos[v] for v in shade_face]
|
|
ax.add_patch(Polygon(poly, closed=True, facecolor=HIGHLIGHT,
|
|
edgecolor='none', zorder=0))
|
|
protected = protected or set()
|
|
for u, v in G.edges(labels=False):
|
|
e = frozenset([u, v])
|
|
c = C[coloring[e]] if coloring is not None else GRAY
|
|
lw = 3.8 if e in protected else 1.4
|
|
(x0, y0), (x1, y1) = pos[u], pos[v]
|
|
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
|
|
for v in G.vertices(sort=False):
|
|
x, y = pos[v]
|
|
if isinstance(v, tuple) and v[0] == 'v_n':
|
|
t = v[1]
|
|
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
|
|
edgecolors='black', linewidths=1.2, zorder=4)
|
|
ax.annotate(f'$v_n^{{({t})}}$', (x, y),
|
|
textcoords='offset points', xytext=(16, 16),
|
|
ha='left', fontsize=14, fontweight='bold',
|
|
color=DARK, zorder=6,
|
|
bbox=dict(boxstyle='round,pad=0.2', fc='white',
|
|
ec=DARK, lw=0.6))
|
|
else:
|
|
ax.scatter(x, y, s=70, color=DARK, zorder=3)
|
|
ax.set_aspect('equal')
|
|
ax.axis('off')
|
|
|
|
|
|
def main():
|
|
G = build_dodecahedron()
|
|
pos = dodecahedron_positions()
|
|
F_v = [('a', i) for i in range(5)]
|
|
|
|
# ----- Step 0: G' with F_v shaded -----
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
draw(ax, G, pos, shade_face=F_v)
|
|
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step0.png'),
|
|
dpi=170, bbox_inches='tight')
|
|
plt.close(fig)
|
|
|
|
# ----- Step 1: Definition 2.1 at F_v with i_1 = 0 -----
|
|
safe = find_safe_pentagonal_face(G, set())
|
|
boundary_1, externals_1, A_1 = safe
|
|
G1 = G.copy()
|
|
for v in boundary_1:
|
|
G1.delete_vertex(v)
|
|
v_n_1 = ('v_n', 1)
|
|
G1.add_vertex(v_n_1)
|
|
G1.add_edge(v_n_1, A_1[0])
|
|
G1.add_edge(v_n_1, A_1[1])
|
|
G1.add_edge(v_n_1, A_1[2])
|
|
G1.add_edge(A_1[3], A_1[4])
|
|
G1.is_planar(set_embedding=True)
|
|
pos1 = {v: p for v, p in pos.items() if v not in boundary_1}
|
|
cx = (pos[A_1[0]][0] + pos[A_1[1]][0] + pos[A_1[2]][0]) / 3
|
|
cy = (pos[A_1[0]][1] + pos[A_1[1]][1] + pos[A_1[2]][1]) / 3
|
|
pos1[v_n_1] = (cx * 0.55, cy * 0.55)
|
|
|
|
cols = edge_coloring(G1, value_only=False)
|
|
coloring = {}
|
|
for k, edge_list in enumerate(cols):
|
|
for u, v in edge_list:
|
|
coloring[frozenset([u, v])] = k
|
|
|
|
E = {
|
|
frozenset([v_n_1, A_1[1]]), # spike
|
|
frozenset([v_n_1, A_1[0]]), # side-0
|
|
frozenset([v_n_1, A_1[2]]), # side-1
|
|
frozenset([A_1[3], A_1[4]]), # merged
|
|
}
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
draw(ax, G1, pos1, coloring=coloring, protected=E)
|
|
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step1.png'),
|
|
dpi=170, bbox_inches='tight')
|
|
plt.close(fig)
|
|
|
|
# ----- Step 2: reduce at the only remaining safe face (outer pentagon) -----
|
|
safe = find_safe_pentagonal_face(G1, E)
|
|
if safe is None:
|
|
print("ERROR: expected an outer pentagonal face but none found.")
|
|
return
|
|
boundary_2, externals_2, A_2 = safe
|
|
f_vec = [coloring[e] for e in externals_2]
|
|
choices = valid_indices(f_vec)
|
|
if not choices:
|
|
print(f"ERROR: f-vector {f_vec} has no valid index.")
|
|
return
|
|
i_t = choices[0]
|
|
|
|
G2 = G1.copy()
|
|
for v in boundary_2:
|
|
G2.delete_vertex(v)
|
|
v_n_2 = ('v_n', 2)
|
|
G2.add_vertex(v_n_2)
|
|
G2.add_edge(v_n_2, A_2[i_t])
|
|
G2.add_edge(v_n_2, A_2[(i_t + 1) % 5])
|
|
G2.add_edge(v_n_2, A_2[(i_t + 2) % 5])
|
|
G2.add_edge(A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5])
|
|
G2.is_planar(set_embedding=True)
|
|
|
|
coloring2 = {e: c for e, c in coloring.items()
|
|
if not any(u in boundary_2 for u in e)}
|
|
side_0_2 = frozenset([v_n_2, A_2[i_t]])
|
|
spike_2 = frozenset([v_n_2, A_2[(i_t + 1) % 5]])
|
|
side_1_2 = frozenset([v_n_2, A_2[(i_t + 2) % 5]])
|
|
merged_2 = frozenset([A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5]])
|
|
coloring2[side_0_2] = coloring[externals_2[i_t]]
|
|
coloring2[spike_2] = coloring[externals_2[(i_t + 1) % 5]]
|
|
coloring2[side_1_2] = coloring[externals_2[(i_t + 2) % 5]]
|
|
coloring2[merged_2] = coloring[externals_2[(i_t + 3) % 5]]
|
|
|
|
pos2 = {v: p for v, p in pos1.items() if v not in boundary_2}
|
|
nbrs = [A_2[i_t], A_2[(i_t + 1) % 5], A_2[(i_t + 2) % 5]]
|
|
cx = sum(pos2[a][0] for a in nbrs) / 3
|
|
cy = sum(pos2[a][1] for a in nbrs) / 3
|
|
r = math.hypot(cx, cy)
|
|
# v_n^{(2)} lies outside the surviving graph (the deleted d's were outermost)
|
|
target_r = 5.0
|
|
pos2[v_n_2] = (cx * target_r / r, cy * target_r / r)
|
|
|
|
E |= {side_0_2, spike_2, side_1_2, merged_2}
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 8))
|
|
draw(ax, G2, pos2, coloring=coloring2, protected=E)
|
|
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
|
|
dpi=170, bbox_inches='tight')
|
|
plt.close(fig)
|
|
|
|
print(f"Wrote fig_alg_step{{0,1,2}}.png to {OUT_DIR}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|