Files
math-research/papers/face_monochromatic_pairs/experiments/counterexample_conj_5_5.py
T
didericis 34141322ce face_monochromatic_pairs: explicit counterexample to Conjecture 5.5
Adds the concrete construction (40 vertices, 60 edges, cubic + planar
+ proper 3-edge-coloured) on which h_φ is simultaneously constant on
two Kempe cycles sharing an edge:

  - K_{red, blue}  = 8-cycle (the outer frame): all h_φ = -1
  - K_{red, green} = 12-cycle (outer frame + upper-left ladder side):
                      all h_φ = -1
  - They share the colour-red edge (0, 7) (and others).

The graph is drawn in TikZiT and stored as
  papers/face_monochromatic_pairs/constant_heawood_counterexample.tikz

The Sage transcription + Heawood/Kempe verification + PNG renderer is
  papers/face_monochromatic_pairs/experiments/counterexample_conj_5_5.py

Rendered PNG (with the four bent outer-face / trapezoid arcs matching
the tikz drawing) is at
  papers/face_monochromatic_pairs/figures/no-two-constant-kempe-counterexample.png

Globally h_φ has 16 vertices at +1 and 24 at -1; the +1 vertices are
concentrated in the inner "tilted ladder" region, leaving the outer
and the K_{red,green}-extension all at -1. This is the structural
reason both Kempe cycles can be constant.

Also includes the TikZiT styles file default.tikzstyles defining the
red/blue/green edge styles used by the .tikz file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 02:55:35 -04:00

372 lines
13 KiB
Python

"""Counterexample to Conjecture 5.5 (transcribed from
`constant_heawood_counterexample.tikz`).
This script:
(1) builds H from the edge list transcribed from the .tikz file;
(2) verifies H is simple, cubic, planar;
(3) verifies the colour assignment is a proper 3-edge-colouring;
(4) computes h_φ at every vertex via the CW rotation around each
vertex using the planar coordinates given in the .tikz file
(rather than Sage's combinatorial embedding, which may pick a
different rotation system);
(5) identifies the {a,b}-, {a,c}-, {b,c}-Kempe cycles of φ;
(6) reports whether h_φ is constant on each Kempe cycle;
(7) saves a PNG of the planar drawing with edges coloured red /
blue / green at the same positions as the .tikz file.
Convention:
colour 0 = red = "a",
colour 1 = blue = "b",
colour 2 = green = "c".
Run with: sage experiments/counterexample_conj_5_5.py
"""
import math
import os
from sage.all import Graph
HERE = os.path.dirname(os.path.abspath(__file__))
OUT_PNG = os.path.join(HERE, '..', 'figures',
'no-two-constant-kempe-counterexample.png')
RED, BLUE, GREEN = 0, 1, 2
COLOUR_NAME = {RED: 'red', BLUE: 'blue', GREEN: 'green'}
# Vertex positions transcribed verbatim from
# constant_heawood_counterexample.tikz.
POS = {
0: (-4.0, 4.5),
1: ( 0.0, 4.0),
2: ( 4.5, 4.5),
3: ( 4.0, 0.0),
4: ( 4.5, -4.0),
5: ( 0.0, -4.0),
6: (-4.0, -4.0),
7: (-4.0, 0.0),
8: (-2.5, 1.5),
9: (-2.0, 2.0),
10: (-1.5, 2.5),
11: (-1.0, 3.0),
12: (-0.5, 3.5),
13: (-3.0, 1.0),
14: (-3.5, 0.5),
15: ( 0.5, 4.5),
16: ( 0.5, -3.5),
17: ( 1.0, -3.0),
18: ( 1.5, -2.5),
19: ( 2.0, -2.0),
20: ( 2.5, -1.5),
21: ( 3.0, -1.0),
22: ( 3.5, -0.5),
23: ( 4.5, 0.5),
24: (-3.5, 1.25),
25: (-2.5, 2.25),
26: (-1.5, 3.25),
27: (-0.5, 4.25),
28: (-2.0, 0.0),
29: ( 0.0, -2.0),
30: ( 2.0, 0.0),
31: ( 0.0, 2.0),
32: (-1.0, 1.0),
33: ( 1.0, 3.0),
34: ( 1.0, -1.0),
35: ( 3.0, 1.0),
36: ( 1.25, -3.5),
37: ( 2.25, -2.5),
38: ( 3.25, -1.5),
39: ( 4.25, -0.5),
}
# Edges with colours, transcribed from the .tikz edgelayer in order.
# Two edges (6-4 and 0-2) are drawn bent in TikZ and represent the
# "outer-face" green arcs; their straight-line geometry in POS would
# pass through the interior, but the embedding treats them as edges of
# the outer face (i.e. they leave each endpoint pointing away from the
# interior). We model this by overriding the angle at vertices 0, 2,
# 6, 4 for these two edges (see CW_ANGLE_OVERRIDE below).
EDGES = [
# blue (4)
(0, 15, BLUE), (2, 23, BLUE), (5, 4, BLUE), (7, 6, BLUE),
# red (outer 4)
(0, 7, RED), (15, 2, RED), (23, 4, RED), (5, 6, RED),
# red (interior-segments group 1)
(14, 13, RED), (8, 9, RED), (10, 11, RED), (12, 1, RED),
(16, 17, RED), (18, 19, RED), (20, 21, RED), (22, 3, RED),
# green (outer "spokes" connecting outer frame to inner parallelograms)
(7, 14, GREEN), (13, 8, GREEN), (9, 10, GREEN), (11, 12, GREEN),
(1, 15, GREEN), (5, 16, GREEN), (17, 18, GREEN), (19, 20, GREEN),
(21, 22, GREEN), (3, 23, GREEN),
# green outer-face arcs
(6, 4, GREEN), # bottom arc (bent in tikz: out=-135, in=-45)
(0, 2, GREEN), # top arc (bent in tikz: bend left=45)
# inner upper-left "trapezoid" connecting 14, 8, 10, 12 to 24..27
(24, 14, BLUE), (25, 8, BLUE), (26, 10, BLUE), (27, 12, BLUE),
(24, 25, RED), (26, 27, RED),
(25, 26, GREEN),
(24, 27, GREEN), # bent
# mid-layer edges connecting outer-frame intermediate vertices to
# the inner "diamond" of 28-35
(13, 28, BLUE), (9, 32, BLUE), (11, 31, BLUE), (1, 33, BLUE),
(29, 17, BLUE), (34, 19, BLUE), (30, 21, BLUE), (35, 3, BLUE),
# lower-right "trapezoid"
(16, 36, BLUE), (18, 37, BLUE), (20, 38, BLUE), (22, 39, BLUE),
(36, 37, RED), (38, 39, RED),
(38, 37, GREEN),
(36, 39, GREEN), # bent
# inner diamond edges (red rungs)
(28, 32, RED), (31, 33, RED), (30, 35, RED), (29, 34, RED),
# inner diamond edges (green rungs)
(32, 34, GREEN), (28, 29, GREEN), (31, 30, GREEN), (33, 35, GREEN),
]
# For the two outer-face green arcs (and the bent edges 24-27 and
# 36-39), override the angle used in the CW-rotation computation so
# that the cyclic order around each endpoint matches the planar
# embedding actually drawn in tikz rather than the straight-line
# geometry.
#
# Convention: angle is measured in degrees, counterclockwise from
# +x axis. The override gives the direction the edge LEAVES the
# endpoint along the tikz-drawn curve, not toward the other endpoint
# of the straight line.
#
# - 0->2 green arc bends "left" over the top; from 0 it leaves
# roughly upward (north), and from 2 it arrives from the upper-left
# (so 2's local angle is also roughly upward).
# - 6->4 green arc bends "around the bottom"; from 6 it leaves
# roughly downward (south), and from 4 it leaves roughly downward.
# - 24->27 green arc bends "left" above the upper-left ladder
# (in=-135, out=-45 conceptually).
# - 36->39 green arc bends "right" below the lower-right ladder.
CW_ANGLE_OVERRIDE = {
(0, 2): 90.0, # at 0, edge to 2 leaves upward
(2, 0): 90.0, # at 2, edge to 0 leaves upward
(6, 4): -90.0, # at 6, edge to 4 leaves downward
(4, 6): -90.0, # at 4, edge to 6 leaves downward
(24, 27): 135.0, # at 24, edge to 27 leaves upper-left
(27, 24): 135.0, # at 27, edge to 24 leaves upper-left
(36, 39): -45.0, # at 36, edge to 39 leaves lower-right
(39, 36): -45.0, # at 39, edge to 36 leaves lower-right
}
def angle_at(v, u):
"""Direction (degrees) from v toward u in the planar drawing,
honouring CW_ANGLE_OVERRIDE for bent edges."""
if (v, u) in CW_ANGLE_OVERRIDE:
return CW_ANGLE_OVERRIDE[(v, u)]
(vx, vy) = POS[v]
(ux, uy) = POS[u]
return math.degrees(math.atan2(uy - vy, ux - vx))
def build_graph(edges):
G = Graph(multiedges=False, loops=False)
for u, v, _ in edges:
G.add_edge(u, v)
return G
def edge_colour_map(edges):
return {frozenset((u, v)): c for u, v, c in edges}
def is_proper_3_edge_colouring(G, col):
for v in G.vertex_iterator():
cols = sorted(col[frozenset((v, u))] for u in G.neighbors(v))
if cols != [RED, BLUE, GREEN]:
return False, v, cols
return True, None, None
def cw_neighbours(G, v):
"""List of v's neighbours in CW order from the planar coordinates
(CW = decreasing polar angle)."""
nbrs = list(G.neighbors(v))
nbrs.sort(key=lambda u: -angle_at(v, u))
return nbrs
def heawood_numbers(G, col):
h = {}
for v in G.vertex_iterator():
nbrs = cw_neighbours(G, v)
cols = [col[frozenset((v, u))] for u in nbrs]
i0 = cols.index(RED)
rot = cols[i0:] + cols[:i0]
if rot == [RED, BLUE, GREEN]:
h[v] = +1
elif rot == [RED, GREEN, BLUE]:
h[v] = -1
else:
raise RuntimeError(f"bad rotation at {v}: {cols}")
return h
def kempe_cycle(G, col, start_edge, two_colours):
target = set(two_colours)
u0, v0 = start_edge
walk = [u0, v0]
while True:
cur, prev = walk[-1], walk[-2]
nxt = None
for u in G.neighbors(cur):
if u == prev:
continue
if col[frozenset((cur, u))] in target:
nxt = u
break
if nxt is None:
raise RuntimeError(f"Kempe walk stuck at {cur}")
if nxt == walk[0] and cur == walk[-2]:
# safety; shouldn't trigger in normal walks
break
walk.append(nxt)
if walk[-1] == walk[0] and len(walk) > 2:
return walk[:-1]
def report(name, cycle, h, two_colours):
h_vals = [h[v] for v in cycle]
plus = sum(1 for x in h_vals if x == +1)
minus = sum(1 for x in h_vals if x == -1)
const = (plus == 0 or minus == 0)
a, b = two_colours
print(f" {name} (colours {{ {COLOUR_NAME[a]}, {COLOUR_NAME[b]} }}):")
print(f" length = {len(cycle)}")
print(f" vertices= {cycle}")
print(f" h_φ = {h_vals}")
print(f" +/- = {plus} (+1) / {minus} (-1)")
print(f" const? {'YES' if const else 'no'}")
return const
# Bent edges, matching the tikz embedding. For each, we give a "rad"
# value (positive => arc bulges to the LEFT of the directed edge u->v
# in matplotlib's connectionstyle convention).
BENT_EDGES = {
# In matplotlib's arc3, positive rad bulges to the RIGHT of the
# directed edge from posA to posB. We pick signs so the bulge
# matches the tikz drawing.
frozenset((0, 2)): ('arc', (0, 2), -0.30), # 0→2 rightward, bulge up (left of dir)
frozenset((6, 4)): ('arc', (6, 4), +0.30), # 6→4 rightward, bulge down (right of dir)
frozenset((24, 27)): ('arc', (24, 27), -0.30), # 24→27 up-right, bulge up-left ("bend left")
frozenset((36, 39)): ('arc', (36, 39), +0.30), # 36→39 up-right, bulge down-right ("bend right")
}
def draw_png(G, col, out_path):
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
os.makedirs(os.path.dirname(out_path), exist_ok=True)
fig, ax = plt.subplots(figsize=(10, 10), dpi=160)
ax.set_aspect('equal')
ax.axis('off')
# Draw edges first so vertices sit on top
for e in G.edge_iterator(labels=False):
u, v = e
c = COLOUR_NAME[col[frozenset(e)]]
if frozenset(e) in BENT_EDGES:
_, (a, b), rad = BENT_EDGES[frozenset(e)]
# Orient so the rad sign matches the (a -> b) direction
if (u, v) != (a, b):
rad = -rad
arc = FancyArrowPatch(
POS[u], POS[v],
arrowstyle='-',
connectionstyle=f'arc3,rad={rad}',
color=c, linewidth=2.5,
shrinkA=0, shrinkB=0,
)
ax.add_patch(arc)
else:
(x1, y1), (x2, y2) = POS[u], POS[v]
ax.plot([x1, x2], [y1, y2], color=c, linewidth=2.5,
solid_capstyle='round')
# Draw vertices
for v, (x, y) in POS.items():
ax.plot(x, y, 'o', markersize=16,
markerfacecolor='lightgrey',
markeredgecolor='black', markeredgewidth=1.0)
ax.text(x, y, str(v), ha='center', va='center', fontsize=8)
# Set bounds with padding (the top arc goes well above 4.5)
xs = [p[0] for p in POS.values()]
ys = [p[1] for p in POS.values()]
pad = 1.5
ax.set_xlim(min(xs) - pad, max(xs) + pad)
ax.set_ylim(min(ys) - pad, max(ys) + pad)
fig.tight_layout()
fig.savefig(out_path, dpi=160, bbox_inches='tight')
plt.close(fig)
print(f"\nWrote: {out_path}")
def main():
print("Loading edges from transcribed tikz...")
G = build_graph(EDGES)
col = edge_colour_map(EDGES)
print(f" |V(H)| = {G.order()}, |E(H)| = {G.size()}")
degs = sorted(set(G.degree()))
print(f" degree set = {degs}")
if degs != [3]:
print(" !! NOT CUBIC"); draw_png(G, col, OUT_PNG); return
print(" cubic ✓")
is_planar = G.is_planar(set_embedding=True)
print(f" planar (by Sage)? {is_planar}")
if not is_planar:
print(" !! NOT PLANAR"); draw_png(G, col, OUT_PNG); return
ok, bad, cols = is_proper_3_edge_colouring(G, col)
if not ok:
print(f" !! NOT a proper 3-edge-colouring: {bad} has {cols}")
draw_png(G, col, OUT_PNG); return
print(" proper 3-edge-colouring ✓")
h = heawood_numbers(G, col)
print("\nHeawood numbers per vertex (using tikz coordinates):")
for v in sorted(h):
print(f" h_φ({v:2d}) = {h[v]:+d}")
print("\nGlobal Heawood sums:")
plus = sum(1 for v in h if h[v] == +1)
minus = sum(1 for v in h if h[v] == -1)
print(f" total: {plus} (+1) / {minus} (-1), sum = {plus - minus}")
# Try all three Kempe-cycle pairs and identify the ones sharing an
# edge. We pick an edge of each colour and trace each cycle.
print("\nAll three Kempe-cycle types through some shared edge:")
# find an edge of each colour
edge_by_color = {}
for u, v, c in EDGES:
if c not in edge_by_color:
edge_by_color[c] = (u, v)
for a in (RED, BLUE, GREEN):
b, c = [x for x in (RED, BLUE, GREEN) if x != a]
e_a = edge_by_color[a]
Kab = kempe_cycle(G, col, e_a, (a, b))
Kac = kempe_cycle(G, col, e_a, (a, c))
print(f"\n-- shared edge: colour-{COLOUR_NAME[a]} edge {e_a}")
const_ab = report(f"K_{{a,b}}={{{COLOUR_NAME[a]},{COLOUR_NAME[b]}}}",
Kab, h, (a, b))
const_ac = report(f"K_{{a,c}}={{{COLOUR_NAME[a]},{COLOUR_NAME[c]}}}",
Kac, h, (a, c))
if const_ab and const_ac:
print(f" ** counterexample with a={COLOUR_NAME[a]} **")
draw_png(G, col, OUT_PNG)
if __name__ == '__main__':
main()