face_monochromatic_pairs: reduce Conj 5.1 to a "deciding face" conjecture

NEW PROOF STRATEGY for Conjecture 5.1 (face-monochromatic-pair):

1. NEW Conjecture (Deciding face): For every chord-apex+Kempe
   colouring φ of every reduced dual, the reduced dual has a face f
   with ∂f ⊆ V(K_b) ∪ V(K_c) and |f| ≢ 0 (mod 3).

2. NEW Theorem: Deciding-face conjecture implies Conj 5.1.

   Proof: contradiction. Assume no clauses-(1)-(3) witness for some
   chord-apex+Kempe φ. By Lemma 5.3, h_φ ≡ ε ∈ {±1} on V(K_b) ∪ V(K_c).
   By the deciding-face conjecture, ∃ face f with ∂f ⊆ V(K_b) ∪ V(K_c),
   |f| ≢ 0 (mod 3). Heawood's face-sum identity (Heawood 1898) gives
   Σ_{v ∈ ∂f} h_φ(v) = ε|f| ≡ 0 (mod 3). Since gcd(|f|, 3) = 1, we get
   ε ≡ 0 (mod 3), but ε ∈ {±1} — contradiction.

3. EMPIRICAL: Conjecture (Deciding face) verified on 142,812 / 142,812
   chord-apex+Kempe colourings of reduced duals up to |V(G)| ≤ 20 --
   matching the full coverage of check_constancy_obstruction.py.
   Face-length distribution:
     |f| = 4:  13,074
     |f| = 5: 102,498 (most common)
     |f| = 7:  18,570
     |f| = 8:   7,752
     |f| = 10:    846
     |f| = 11:     72
   (All ≢ 0 mod 3.)

New scripts:
  - check_kb_kc_coverage.py: |V(K_b) ∪ V(K_c)| / |V(Ĝ')| distribution.
    73.87% of colourings have V(K_b) ∪ V(K_c) = V (full coverage); the
    remaining 26% have coverage ≥ 70%, mostly ≥ 90%.
  - check_deciding_face.py: existence of deciding face across all
    colourings; 100.00% / 142,812.

Why this is the right reduction:
  - It uses ALL THREE pieces of chord-apex+Kempe structure: Lemma 5.3
    (constancy from no-witness), forced colour-equality at merged/spike,
    and forced Kempe-cycle containment of merged + spike + side edges
    (the latter two enter via V(K_b) ∪ V(K_c) covering specific
    structural vertices).
  - It uses Heawood's face-sum identity, which is the classical 3-fold
    parity constraint on cubic plane 3-edge-colourings.
  - The C28 counterexample to Conjecture 5.5 is not affected: it's not
    a chord-apex+Kempe colouring of a reduced dual, so the deciding-face
    structure doesn't apply.

Remaining work: prove the deciding-face conjecture structurally (likely
via the specific F_01 / F_12 "flank face" of the reduced dual, whose
length n_0 - 1 from the adjacent G'-face of length n_0 ≥ 5 is ≢ 0 mod 3
exactly when n_0 ≢ 1 mod 3, plus boundary-in-V(K_b) ∪ V(K_c) which
follows from Lemma 5.X kempe-spike + colour analysis at A_i).

Paper grows from 17 to 18 pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 04:12:09 -04:00
parent 9103fa7068
commit d7e9b6af2f
4 changed files with 395 additions and 0 deletions
@@ -0,0 +1,161 @@
"""For every chord-apex+Kempe colouring of every reduced dual (up to
|V(G)| ≤ 18), check whether there exists a "deciding face": a face f
of the reduced dual whose boundary lies entirely in V(K_b) V(K_c)
and whose length is not divisible by 3.
If a deciding face exists, then under the (hypothetical) constancy
hypothesis h_φ ≡ ε on V(K_b) V(K_c), Heawood's face-sum identity
gives ε · |f| ≡ 0 (mod 3), which forces ε ≡ 0 since |f| ≢ 0 (mod 3) --
contradicting ε ∈ {±1}. So existence of a deciding face in every
chord-apex+Kempe colouring of every reduced dual would *prove*
Conjecture 5.1 via the contrapositive of Lemma 5.3.
This script reports, per (n_G, colouring): does a deciding face exist?
And if not, why not (insufficient coverage, all in-coverage faces have
length ≡ 0 mod 3, etc.).
Run with: sage experiments/check_deciding_face.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_3_8_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 find_deciding_face(H, V_union):
"""Return (face, |face| mod 3) of a deciding face (all boundary in
V_union, length not divisible by 3). None if no such face exists."""
for face in H.faces():
verts = {u for (u, v) in face} | {v for (u, v) in face}
if not verts.issubset(V_union):
continue
L = len(face)
if L % 3 != 0:
return face, L, L % 3
return None
def test_one(D):
D.is_planar(set_embedding=True)
n_col = 0
n_with_deciding = 0
no_deciding_examples = []
deciding_lengths = {} # |f| of deciding face -> count
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)]
for col in cand:
n_col += 1
merged_idx = edge_idx(edges, named['merged'])
a = col[merged_idx]
bs = [c for c in range(3) if c != a]
kc_b = kempe_cycle_set(edges, col, merged_idx, (a, bs[0]))
kc_c = kempe_cycle_set(edges, col, merged_idx, (a, bs[1]))
V_b = vertices_of_kempe(edges, kc_b)
V_c = vertices_of_kempe(edges, kc_c)
V_union = V_b | V_c
deciding = find_deciding_face(H, V_union)
if deciding is not None:
n_with_deciding += 1
_, L, mod_r = deciding
deciding_lengths[L] = deciding_lengths.get(L, 0) + 1
else:
cov_ratio = len(V_union) / H.order()
in_cov_face_lens = sorted([
len(f) for f in H.faces()
if {u for (u, v) in f} | {v for (u, v) in f}
<= V_union])
if len(no_deciding_examples) < 3:
no_deciding_examples.append({
'cov_ratio': cov_ratio,
'in_cov_face_lens': in_cov_face_lens,
'V_union_size': len(V_union),
'V_total': H.order(),
})
return n_col, n_with_deciding, deciding_lengths, no_deciding_examples
def main(max_n=20, time_budget_per_n=1800):
print(f"Deciding-face existence on chord-apex+Kempe colourings, "
f"n_G ∈ [12, {max_n}]\n")
grand_col = 0
grand_dec = 0
grand_lens = {}
grand_no_dec_examples = []
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_col_n = 0
n_dec_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}/{len(triangulations)}")
break
G.is_planar(set_embedding=True)
D = dual_of(G)
ni, ndec, lens, no_dec_ex = test_one(D)
n_col_n += ni
n_dec_n += ndec
for k, v in lens.items():
grand_lens[k] = grand_lens.get(k, 0) + v
if len(grand_no_dec_examples) < 5:
grand_no_dec_examples.extend(
no_dec_ex[:5 - len(grand_no_dec_examples)])
elapsed = time.time() - start
pct = 100 * n_dec_n / max(n_col_n, 1)
print(f"n={n}: {n_col_n} col., {n_dec_n} with deciding face "
f"({pct:.2f}%) [{elapsed:.0f}s]")
sys.stdout.flush()
grand_col += n_col_n
grand_dec += n_dec_n
print()
print("=" * 70)
print(f"Grand totals: {grand_col} chord-apex+Kempe colourings")
pct = 100 * grand_dec / max(grand_col, 1)
print(f" with deciding face: {grand_dec} ({pct:.2f}%)")
print(f" without : {grand_col - grand_dec} "
f"({100 - pct:.2f}%)")
if grand_lens:
print()
print(" Deciding face length distribution:")
for L in sorted(grand_lens):
print(f" |f| = {L} (mod 3 = {L % 3}): {grand_lens[L]}")
if grand_no_dec_examples:
print()
print(" Sample colourings WITHOUT a deciding face:")
for ex in grand_no_dec_examples[:5]:
print(f" coverage = {ex['V_union_size']}/{ex['V_total']} "
f"({100*ex['cov_ratio']:.0f}%); in-coverage face "
f"lengths = {ex['in_cov_face_lens']}")
if __name__ == '__main__':
main()