4ceae9c68a
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>
216 lines
8.8 KiB
Python
216 lines
8.8 KiB
Python
"""Check whether sub-case (ii.B) of Case (b) of the Flank-covering
|
||
lemma (n_i = 6) ever arises in chord-apex+Kempe colourings.
|
||
|
||
Sub-case (ii.B) is the configuration in which the structural
|
||
propagation argument in the partial proof has a real gap:
|
||
|
||
- n_i = 6: F_flank_{i, i+1}^♭ has 2 intermediates P_1, P_2 on the
|
||
F-arc from A_i to A_{i+1}.
|
||
- Case (b): varphi(A_i P_1) = c_1.
|
||
- Sub-case (ii): varphi(P_1 P_2) = c_0.
|
||
- Derived: varphi(A_{i+1} P_2) = c_1, varphi(P_2 O_2) = c,
|
||
varphi(P_1 O_1) = c, varphi(A_i Q) = c.
|
||
|
||
In this sub-case the lemma's local argument (that the cycle at P_2
|
||
passes from P_2 to P_1) needs P_2 ∈ V(K_b), which isn't forced from
|
||
the local data: the {c, c_0}-Kempe cycle through P_2 might not be K_b.
|
||
|
||
We check: across all chord-apex+Kempe colourings of all reduced duals
|
||
with |V(G)| ≤ 20, does sub-case (ii.B) ever occur? If yes, in how many
|
||
of those configurations is P_1 actually in V(K_b) ∪ V(K_c) (i.e., the
|
||
conclusion of the lemma still holds even though our proof gap is
|
||
real)?
|
||
|
||
Run with: sage experiments/check_subcase_iib.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 find_flank_arc_intermediates(H, named, i, side):
|
||
"""Return the ordered F-arc from A_i (or A_{i+1}) along F's
|
||
boundary as a list of vertices.
|
||
|
||
side='lower': arc from A_i to A_{i+1} (= F_flank_{i, i+1}^♭).
|
||
side='upper': arc from A_{i+1} to A_{i+2} (= F_flank_{i+1, i+2}^♭).
|
||
|
||
We use the planar embedding of H to walk around the
|
||
face containing v_n + side_0 + spike (lower) or v_n + spike + side_1
|
||
(upper).
|
||
"""
|
||
H.is_planar(set_embedding=True)
|
||
spike = named['spike'] # frozenset({A_{i+1}, v_n})
|
||
side_0 = named['side_0']
|
||
side_1 = named['side_1']
|
||
# The flank face contains v_n + spike + side_0/side_1 + the arc edges
|
||
target_edges = set()
|
||
if side == 'lower':
|
||
target_edges = {side_0, spike}
|
||
else:
|
||
target_edges = {side_1, spike}
|
||
for face in H.faces():
|
||
# face is list of (u, v) edges (or tuples of vertex pairs)
|
||
face_edges_set = {frozenset(e) for e in face}
|
||
if target_edges.issubset(face_edges_set):
|
||
# Trace the boundary, return the vertices in cyclic order
|
||
verts = []
|
||
for e in face:
|
||
verts.append(e[0])
|
||
return verts
|
||
return None
|
||
|
||
|
||
def test_one(D):
|
||
D.is_planar(set_embedding=True)
|
||
n_col = 0
|
||
n_subcase_iib = 0
|
||
n_subcase_iib_p1_covered = 0 # of those, how many have P_1 ∈ V(K_b) ∪ V(K_c)
|
||
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
|
||
# We need to identify P_1, P_2, A_i, A_{i+1}, etc.
|
||
# Recover the named vertices from `named`.
|
||
v_n = 9999
|
||
# named['side_0'] = {A_i, v_n}, named['spike'] = {A_{i+1}, v_n}
|
||
# side_1 = {A_{i+2}, v_n}, merged = {A_{i+3}, A_{i+4}}
|
||
A_i = next(iter(named['side_0'] - {v_n}))
|
||
A_ip1 = next(iter(named['spike'] - {v_n}))
|
||
A_ip2 = next(iter(named['side_1'] - {v_n}))
|
||
A_ip3, A_ip4 = sorted(named['merged'])
|
||
# Get flank arc for lower (A_i → A_{i+1})
|
||
# The face F_flank_{i, i+1}^♭ has boundary v_n - A_i - ... - A_{i+1} - v_n
|
||
# Find this face:
|
||
target_edges = {named['side_0'], named['spike']}
|
||
arc_verts = None
|
||
for f in H.faces():
|
||
f_edges = {frozenset(e) for e in f}
|
||
if target_edges.issubset(f_edges):
|
||
arc_verts = [e[0] for e in f]
|
||
break
|
||
if arc_verts is None or len(arc_verts) != 5:
|
||
# Not the n_i = 6 case (length 5 flank face)
|
||
continue
|
||
# Identify P_1, P_2 along the arc
|
||
# The cycle visits v_n, A_i, P_1, P_2, A_{i+1} in some order
|
||
# (possibly reversed). Find the index of v_n.
|
||
if v_n not in arc_verts: continue
|
||
k = arc_verts.index(v_n)
|
||
cyc = arc_verts[k:] + arc_verts[:k] # rotate so v_n is first
|
||
# cyc should be [v_n, A_i, P_1, P_2, A_{i+1}] or
|
||
# [v_n, A_{i+1}, P_2, P_1, A_i]
|
||
if cyc[1] == A_i:
|
||
P_1, P_2 = cyc[2], cyc[3]
|
||
assert cyc[4] == A_ip1
|
||
elif cyc[1] == A_ip1:
|
||
P_2, P_1 = cyc[2], cyc[3]
|
||
assert cyc[4] == A_i
|
||
else:
|
||
continue
|
||
# Check sub-case (ii.B): φ(A_i P_1) = c_1, φ(P_1 P_2) = c_0
|
||
# c = color of merged/spike
|
||
merged_idx = edge_idx(edges, named['merged'])
|
||
c = col[merged_idx]
|
||
# side_0 has color c_0
|
||
side_0_idx = edge_idx(edges, named['side_0'])
|
||
c_0 = col[side_0_idx]
|
||
# side_1 has color c_1
|
||
side_1_idx = edge_idx(edges, named['side_1'])
|
||
c_1 = col[side_1_idx]
|
||
# Get edge colours of (A_i, P_1) and (P_1, P_2)
|
||
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 or col[e_P1P2] != c_0:
|
||
continue
|
||
# Sub-case (ii.B) detected
|
||
n_subcase_iib += 1
|
||
# Check if P_1 ∈ V(K_b) ∪ V(K_c)
|
||
a = c
|
||
other = [x for x in range(3) if x != a]
|
||
# K_b = {a, b_color} Kempe cycle through merged
|
||
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)
|
||
if P_1 in V_b or P_1 in V_c:
|
||
n_subcase_iib_p1_covered += 1
|
||
return n_col, n_subcase_iib, n_subcase_iib_p1_covered
|
||
|
||
|
||
def main(max_n=20, time_budget_per_n=1800):
|
||
print("Check: how often does sub-case (ii.B) of Case (b) of\n"
|
||
"Lemma 'Flank covering, n_i = 6' actually arise?\n"
|
||
f"n_G in [12, {max_n}]\n")
|
||
grand_col = 0
|
||
grand_sub = 0
|
||
grand_sub_covered = 0
|
||
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_sub_n = 0; n_cov_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, ns, ncov = test_one(D)
|
||
n_col_n += ni; n_sub_n += ns; n_cov_n += ncov
|
||
elapsed = time.time() - start
|
||
print(f"n={n}: {n_col_n} col., {n_sub_n} sub-case (ii.B) "
|
||
f"({100*n_sub_n/max(n_col_n, 1):.2f}% of col.); "
|
||
f"{n_cov_n} of those have P_1 ∈ V(K_b)∪V(K_c) "
|
||
f"[{elapsed:.0f}s]")
|
||
sys.stdout.flush()
|
||
grand_col += n_col_n
|
||
grand_sub += n_sub_n
|
||
grand_sub_covered += n_cov_n
|
||
print()
|
||
print("=" * 70)
|
||
print(f"Grand totals: {grand_col} chord-apex+Kempe colourings")
|
||
print(f" sub-case (ii.B) detected: {grand_sub} "
|
||
f"({100*grand_sub/max(grand_col, 1):.2f}%)")
|
||
if grand_sub > 0:
|
||
print(f" of those, P_1 ∈ V(K_b)∪V(K_c): {grand_sub_covered} "
|
||
f"({100*grand_sub_covered/max(grand_sub, 1):.2f}%)")
|
||
gap_open = grand_sub - grand_sub_covered
|
||
print(f" of those, P_1 NOT in V(K_b)∪V(K_c): {gap_open}")
|
||
else:
|
||
print(" Sub-case (ii.B) never arises empirically.")
|
||
print(" → the proof gap is structurally INACTIVE")
|
||
print(" → the n_i = 6 lemma's conclusion is empirically tight")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|