Files
math-research/papers/face_monochromatic_pairs/experiments/check_S_face_structure.py
T
didericis 4ceae9c68a face_monochromatic_pairs: rename check_conj_3_8_scaled → check_conj_final_scaled; add n=21-24 test
Rename the shared helper module to a number-resistant name. Update
all 26 dependent scripts via sed.

Add experiments/test_n_21_to_24.py — extends the empirical check
beyond |V(G)| ≤ 20 to n_G ∈ [21, 24]. Checks per chord-apex+Kempe
colouring:
  (1) h_φ constant on V(K_b)? (counterexample to Corollary 5.4)
  (2) h_φ constant on V(K_b) ∪ V(K_c)? (counterexample to Conj 5.1)
  (3) Deciding face exists?

Writes results incrementally to test_n_21_to_24_results.jsonl (one
JSON line per triangulation, plus n-level and grand summaries).
Emits PROGRESS lines every 10 minutes (default) to stdout for live
monitoring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 08:01:29 -04:00

204 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""For the 1,314 bad chord-apex+Kempe colourings (with sub-case (ii.B)
+ P_1 ∉ V(K_b) V(K_c)), check the *face structure* induced by S:
- Is the S-cycle a face boundary of the reduced dual?
- For each S-edge, what are the 2 adjacent faces?
- How many of those F_ext faces are G'-pentagons?
- Does the bound (# G'-pentagons hit ≤ |S|) hold?
If S being a single cycle is also a face boundary, then # G'-pentagons
hit by S ≤ |S| (one per S-edge, with the F1 interior face being a
|S|-length face = not a pentagon since |S| ≥ 6). Combined with the
p_G ≥ 7 lower bound, this closes |S| ≤ 6 cases.
Run with: sage experiments/check_S_face_structure.py
"""
import os
import sys
import time
from sage.all import Graph
from sage.graphs.graph_generators import graphs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from check_conj_final_scaled import (
apply_reduction,
proper_3_edge_colorings,
matches_chord_apex_kempe,
kempe_cycle_set,
edge_idx,
)
from check_heawood_on_kempe import dual_of, vertices_of_kempe
def test_one(D):
D.is_planar(set_embedding=True)
bad_count = 0
# |S| -> # bad colourings of that size where S is a face boundary
is_face_boundary = {}
# |S| -> # bad colourings of that size
by_size = {}
# |S| -> distribution of (# G'-pentagons hit by S)
pentagons_hit_by_size = {}
# |S| -> distribution of (# G'-pentagons total in reduced dual)
pent_total_by_size = {}
for face in D.faces():
if len(face) != 5: continue
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None: continue
H = res['H']; named = res['named']
H.is_planar(set_embedding=True)
edges, colorings = proper_3_edge_colorings(H)
cand = [c for c in colorings
if matches_chord_apex_kempe(edges, c, named)]
v_n = 9999
for col in cand:
# Identify bad sub-case (ii.B)
target = {named['side_0'], named['spike']}
lower_flank = None
for f in H.faces():
if target.issubset({frozenset(e) for e in f}):
lower_flank = f; break
if lower_flank is None or len(lower_flank) != 5: continue
arc_verts = [e[0] for e in lower_flank]
if v_n not in arc_verts: continue
k = arc_verts.index(v_n)
cyc = arc_verts[k:] + arc_verts[:k]
A_i = next(iter(named['side_0'] - {v_n}))
A_ip1 = next(iter(named['spike'] - {v_n}))
if cyc[1] == A_i and cyc[4] == A_ip1:
P_1, P_2 = cyc[2], cyc[3]
elif cyc[1] == A_ip1 and cyc[4] == A_i:
P_2, P_1 = cyc[2], cyc[3]
else: continue
merged_idx = edge_idx(edges, named['merged'])
c_col = col[merged_idx]
c_0_col = col[edge_idx(edges, named['side_0'])]
c_1_col = col[edge_idx(edges, named['side_1'])]
e_AiP1 = edge_idx(edges, frozenset((A_i, P_1)))
e_P1P2 = edge_idx(edges, frozenset((P_1, P_2)))
if e_AiP1 is None or e_P1P2 is None: continue
if col[e_AiP1] != c_1_col or col[e_P1P2] != c_0_col:
continue
a = c_col
other = [x for x in range(3) if x != a]
kc_b = kempe_cycle_set(edges, col, merged_idx, (a, other[0]))
kc_c = kempe_cycle_set(edges, col, merged_idx, (a, other[1]))
V_b = vertices_of_kempe(edges, kc_b)
V_c = vertices_of_kempe(edges, kc_c)
V_union = V_b | V_c
S = set(H.vertices()) - V_union
if P_1 in V_union: continue
bad_count += 1
S_size = len(S)
by_size[S_size] = by_size.get(S_size, 0) + 1
# Is S a face boundary?
# Find a face whose boundary vertices = S exactly.
S_is_face = False
for f in H.faces():
verts = {u for (u, v) in f} | {v for (u, v) in f}
if verts == S:
S_is_face = True
break
if S_is_face:
is_face_boundary[S_size] = (
is_face_boundary.get(S_size, 0) + 1)
# # G'-pentagons (= not adjacent to F_v's modification)
def is_g_prime_pentagon(f):
if len(f) != 5: return False
f_edges_set = {frozenset(e) for e in f}
if (named['side_0'] in f_edges_set or
named['side_1'] in f_edges_set or
named['spike'] in f_edges_set or
named['merged'] in f_edges_set):
return False
return True
p_total = 0
p_hit = 0
for f in H.faces():
if not is_g_prime_pentagon(f): continue
p_total += 1
verts = {u for (u, v) in f} | {v for (u, v) in f}
if verts & S:
p_hit += 1
pent_dist = pentagons_hit_by_size.setdefault(S_size, {})
pent_dist[p_hit] = pent_dist.get(p_hit, 0) + 1
tot_dist = pent_total_by_size.setdefault(S_size, {})
tot_dist[p_total] = tot_dist.get(p_total, 0) + 1
return bad_count, by_size, is_face_boundary, pentagons_hit_by_size, pent_total_by_size
def main(max_n=20, time_budget_per_n=1800):
print("Face structure of S-cycle in bad chord-apex+Kempe colourings.\n")
grand_bad = 0
grand_size = {}
grand_face_b = {}
grand_pent_hit = {}
grand_pent_tot = {}
for n in range(12, max_n + 1):
start = time.time()
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f"n={n}: cannot enumerate ({ex})")
continue
n_bad_n = 0
for tri_idx, G in enumerate(triangulations):
if time.time() - start > time_budget_per_n:
print(f" n={n}: timeout at tri {tri_idx}")
break
G.is_planar(set_embedding=True)
D = dual_of(G)
nb, bs, ifb, ph, pt = test_one(D)
n_bad_n += nb
for k, v in bs.items(): grand_size[k] = grand_size.get(k, 0) + v
for k, v in ifb.items(): grand_face_b[k] = grand_face_b.get(k, 0) + v
for sz, dist in ph.items():
for k, v in dist.items():
grand_pent_hit.setdefault(sz, {})
grand_pent_hit[sz][k] = grand_pent_hit[sz].get(k, 0) + v
for sz, dist in pt.items():
for k, v in dist.items():
grand_pent_tot.setdefault(sz, {})
grand_pent_tot[sz][k] = grand_pent_tot[sz].get(k, 0) + v
elapsed = time.time() - start
print(f"n={n}: {n_bad_n} bad colourings [{elapsed:.0f}s]")
sys.stdout.flush()
grand_bad += n_bad_n
print()
print("=" * 70)
print(f"Total bad colourings: {grand_bad}")
print("\nIs S-cycle a face boundary of reduced dual?")
for sz in sorted(grand_size):
tot = grand_size[sz]
face_count = grand_face_b.get(sz, 0)
pct = 100 * face_count / max(tot, 1)
print(f" |S| = {sz}: {face_count} / {tot} ({pct:.1f}%) yes")
print("\nDistribution of # G'-pentagons HIT by S, by |S|:")
for sz in sorted(grand_pent_hit):
print(f" |S| = {sz}:")
for h, c in sorted(grand_pent_hit[sz].items()):
print(f" {h} pentagons hit: {c}")
print("\nDistribution of # G'-pentagons TOTAL, by |S|:")
for sz in sorted(grand_pent_tot):
print(f" |S| = {sz}:")
for t, c in sorted(grand_pent_tot[sz].items()):
print(f" {t} pentagons total: {c}")
print("\nKey check: is # hit ≤ |S| always?")
for sz in sorted(grand_pent_hit):
max_hit = max(grand_pent_hit[sz].keys())
leq = max_hit <= sz
print(f" |S| = {sz}: max # hit = {max_hit}, "
f"{'' if leq else '>'} |S| ({'' if leq else '✗ violated'})")
if __name__ == '__main__':
main()