07124b6c95
Audit of the structural proof of Conjecture 5.1 (via deciding-face
conjecture) identifies one real proof gap:
Lemma (Flank covering, n_i = 6), Case (b) sub-case (ii) -- when
φ(A_i P_1) = c_1 AND φ(P_1 P_2) = c_0 -- the propagation argument
"the cycle at P_2 passes from P_2 to P_1" requires the {c, c_0}-Kempe
cycle through P_2 to be K_b, which forces P_1 onto K_b via the c_0
edge P_1 P_2. Properness at P_2 only forces P_2 ∈ V(K_c) (via
φ(A_{i+1} P_2) = c_1), not P_2 ∈ V(K_b). The further step requires
controlling the {c, c_0}-walk through the rest of the graph, which
the local argument doesn't do.
experiments/audit_tight_coverage.py quantifies the impact across
empirical data:
- 7,930 / 7,930 (G, v, i) triples up to |V(G)| ≤ 20 are covered
by the FULL partial proof (including the n_i = 6 lemma);
- 7,531 / 7,930 (94.97%) are covered by the TIGHT subset
(n_i = 5 OR n_{i+1} = 5 OR (n_{i+2}, n_{i+4}) = (5, 5)) which
has no proof gap;
- 399 (5.03%) genuinely require the n_i = 6 lemma.
So the gap matters: empirical coverage of the tight subset alone is
~95%, not 100%.
Paper changes:
- Lemma (Flank covering, n_i = 6) marked as "partial" with a status
note in the statement itself.
- Proof of Lemma includes an "Audit note" identifying the open
sub-case explicitly, after establishing the parts that ARE proven.
- Empirical coverage remark softened: the 100% claim is restated
as "modulo the open sub-case", with the 94.97% tight figure
given separately.
Empirically the n_i = 6 lemma is robust (all 142,812 colourings have
a deciding face), so the gap is probably patchable — likely either
via a structural argument that rules out the bad sub-case in
chord-apex+Kempe colourings, or via a global K_b-walk argument
showing P_2 ∈ V(K_b) anyway. But this is open.
Paper stays at 21 pages (only added text within existing lemma + remark).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
178 lines
7.7 KiB
Python
178 lines
7.7 KiB
Python
"""For every reduction (G, v, i) underlying a chord-apex+Kempe
|
|
colouring (up to |V(G)| ≤ 20), check the degrees of v's five
|
|
neighbours in G.
|
|
|
|
This determines whether the partial structural proof of the
|
|
deciding-face conjecture (Theorem~\\ref{thm:deciding-face-partial} in
|
|
the paper) covers ALL configurations empirically:
|
|
|
|
- If every (G, v) we encounter has at least one neighbour of v with
|
|
deg_G(·) ≤ 6, the partial proof (which requires at least one
|
|
n_k ∈ {5, 6}, equivalently at least one u_{k+1} with deg_G ≤ 6)
|
|
handles every chord-apex+Kempe colouring up to |V(G)| ≤ 20.
|
|
|
|
- If some (G, v) has all five neighbours of degree ≥ 7, we'd need
|
|
the unproved merged-side argument for those.
|
|
|
|
Reports per n_G: number of (G, v) pairs with all-≥-7 neighbours.
|
|
|
|
Run with: sage experiments/check_v_neighbour_degrees.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)
|
|
|
|
|
|
def cyclic_neighbour_degrees(G, v):
|
|
"""Return the cyclic sequence of degrees of v's neighbours, in
|
|
their CW order around v in G's planar embedding."""
|
|
G.is_planar(set_embedding=True)
|
|
emb = G.get_embedding()
|
|
return [G.degree(u) for u in emb[v]]
|
|
|
|
|
|
def main(max_n=20, time_budget_per_n=1200):
|
|
print("For every (G, v, i) underlying a chord-apex+Kempe colouring, "
|
|
"check the\ndegrees of v's neighbours in G.\n")
|
|
print("Each reduction index i ∈ {0,…,4} picks two consecutive "
|
|
"neighbours\n(u_{i+1}, u_{i+2}) of v whose degrees become the\n"
|
|
"two flank-face adjacent G'-face lengths n_i, n_{i+1}.\n")
|
|
print(f"n_G in [12, {max_n}]\n")
|
|
|
|
grand_triples = 0 # total (G, v, i) triples
|
|
grand_bad_triples = 0 # both n_i, n_{i+1} ≥ 7
|
|
grand_pairs = 0 # (G, v) pairs
|
|
grand_all_ge_7 = 0 # (G, v) pairs with all 5 neighbours deg ≥ 7
|
|
grand_dist_min = {} # min neighbour deg over (G, v)
|
|
grand_dist_consec = {} # min over consecutive pair (n_i, n_{i+1}) for some i
|
|
grand_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
|
|
triples_n = 0
|
|
bad_triples_n = 0
|
|
pairs_n = 0
|
|
all_ge_7_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
|
|
for v in G.vertex_iterator():
|
|
if G.degree(v) != 5:
|
|
continue
|
|
cyc = cyclic_neighbour_degrees(G, v)
|
|
pairs_n += 1
|
|
m_all = min(cyc)
|
|
grand_dist_min[m_all] = grand_dist_min.get(m_all, 0) + 1
|
|
if m_all >= 7:
|
|
all_ge_7_n += 1
|
|
worst_consec_min = max(min(cyc[i], cyc[(i+1) % 5])
|
|
for i in range(5))
|
|
grand_dist_consec[worst_consec_min] = (
|
|
grand_dist_consec.get(worst_consec_min, 0) + 1)
|
|
for i in range(5):
|
|
triples_n += 1
|
|
n_i = cyc[(i + 1) % 5]
|
|
n_ip1 = cyc[(i + 2) % 5]
|
|
n_ip2 = cyc[(i + 3) % 5]
|
|
n_ip3 = cyc[(i + 4) % 5]
|
|
n_ip4 = cyc[(i + 5) % 5]
|
|
# AUDIT: does the structurally-tight subset (drop n_i=6 case) cover?
|
|
tight_covered = (
|
|
n_i == 5 or n_ip1 == 5 or
|
|
(n_ip2 == 5 and n_ip4 == 5)
|
|
)
|
|
if not tight_covered:
|
|
# Record what would be the "audit-uncovered" triples
|
|
# (= we have proved coverage only on n_i = 5 or n_{i+1} = 5
|
|
# or (n_{i+2}, n_{i+4}) = (5, 5)).
|
|
pass # counted below as "audit_uncovered_n"
|
|
if n_i >= 7 and n_ip1 >= 7:
|
|
bad_triples_n += 1
|
|
# Check whether F_outer's length condition is OK
|
|
outer_len = n_ip2 + n_ip4 - 3
|
|
outer_ok = (outer_len % 3 != 0) and (n_ip2 == 5 and n_ip4 == 5)
|
|
if not outer_ok or len(grand_examples) < 10:
|
|
grand_examples.append({
|
|
'n_G': n, 'tri_idx': tri_idx, 'v': v, 'i': i,
|
|
'cyclic_degs': cyc,
|
|
'n_i': n_i, 'n_ip1': n_ip1,
|
|
'n_ip2': n_ip2, 'n_ip3': n_ip3, 'n_ip4': n_ip4,
|
|
'outer_len': outer_len,
|
|
'outer_ok': outer_ok,
|
|
'graph6': G.canonical_label().graph6_string(),
|
|
})
|
|
elapsed = time.time() - start
|
|
print(f"n={n}: {pairs_n} (G,v) pairs, {triples_n} (G,v,i) triples; "
|
|
f"{bad_triples_n} triples with both flank-adj deg ≥ 7 "
|
|
f"[{elapsed:.0f}s]")
|
|
sys.stdout.flush()
|
|
grand_pairs += pairs_n
|
|
grand_triples += triples_n
|
|
grand_bad_triples += bad_triples_n
|
|
grand_all_ge_7 += all_ge_7_n
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print(f"Grand totals across n_G in [12, {max_n}]:")
|
|
print(f" (G, v) pairs: {grand_pairs}")
|
|
print(f" (G, v, i) triples: {grand_triples}")
|
|
pct = 100 * grand_all_ge_7 / max(grand_pairs, 1)
|
|
print(f" pairs with all 5 neighbours deg ≥ 7: "
|
|
f"{grand_all_ge_7} ({pct:.2f}%)")
|
|
pct = 100 * grand_bad_triples / max(grand_triples, 1)
|
|
print(f" triples with BOTH flank-adj deg ≥ 7: "
|
|
f"{grand_bad_triples} ({pct:.2f}%)")
|
|
print()
|
|
print(" Distribution of min(all 5 neighbour degs) per (G, v):")
|
|
for m in sorted(grand_dist_min):
|
|
c = grand_dist_min[m]
|
|
ppct = 100 * c / max(grand_pairs, 1)
|
|
bar = '#' * max(1, int(ppct))
|
|
print(f" {m}: {c:>6} ({ppct:5.2f}%) {bar[:50]}")
|
|
print()
|
|
print(" Distribution of MAX over i of "
|
|
"min(n_i, n_{i+1}) per (G, v)\n"
|
|
" (i.e., the worst-case consecutive-pair-min --- if this is\n"
|
|
" ≤ 6, every i has min(n_i, n_{i+1}) ≤ 6 and the partial proof\n"
|
|
" applies; if it is ≥ 7, at least one i has both flanks ≥ 7):")
|
|
for m in sorted(grand_dist_consec):
|
|
c = grand_dist_consec[m]
|
|
ppct = 100 * c / max(grand_pairs, 1)
|
|
bar = '#' * max(1, int(ppct))
|
|
print(f" {m}: {c:>6} ({ppct:5.2f}%) {bar[:50]}")
|
|
if grand_examples:
|
|
print()
|
|
n_outer_ok = sum(1 for ex in grand_examples if ex['outer_ok'])
|
|
n_outer_bad = sum(1 for ex in grand_examples if not ex['outer_ok'])
|
|
print(f" All {len(grand_examples)} (G, v, i) triples with BOTH "
|
|
f"flank-adj deg ≥ 7:")
|
|
print(f" {n_outer_ok} have n_{{i+2}}=n_{{i+4}}=5 "
|
|
f"(F_outer candidate OK)")
|
|
print(f" {n_outer_bad} do NOT (F_outer might not apply)")
|
|
print()
|
|
for ex in grand_examples:
|
|
tag = "[F_outer OK]" if ex['outer_ok'] else "[F_outer FAILS]"
|
|
print(f" {tag} n={ex['n_G']}, tri#{ex['tri_idx']}, "
|
|
f"v={ex['v']}, i={ex['i']}: "
|
|
f"cyc={ex['cyclic_degs']}, "
|
|
f"(n_i,n_{{i+1}},n_{{i+2}},n_{{i+3}},n_{{i+4}}) = "
|
|
f"({ex['n_i']},{ex['n_ip1']},{ex['n_ip2']},"
|
|
f"{ex['n_ip3']},{ex['n_ip4']}), "
|
|
f"F_outer len = {ex['outer_len']}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|