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>
215 lines
8.9 KiB
Python
215 lines
8.9 KiB
Python
"""Simpler version: enumerate the residual (|S|=8, hit=p_total=8)
|
|
colourings without requiring v_parent identification. For each,
|
|
determine:
|
|
- the n_k sequence of the reduction (from the reduced dual's
|
|
F_k structure),
|
|
- whether any G'-face (length ≢ 0 mod 3) is uncovered.
|
|
|
|
The earlier check_S_face_structure.py showed at most 30 |S|=8 cases
|
|
with hit = 8, but didn't constrain p_total. Of those, ≤30 have
|
|
p_total = 8 (= the residual).
|
|
|
|
Run with: sage experiments/check_30_residual_v2.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 is_g_prime_pentagon(f, named):
|
|
if len(f) != 5: return False
|
|
fset = {frozenset(e) for e in f}
|
|
return not (named['side_0'] in fset or named['side_1'] in fset
|
|
or named['spike'] in fset or named['merged'] in fset)
|
|
|
|
|
|
def is_g_prime_face(f, named):
|
|
fset = {frozenset(e) for e in f}
|
|
return not (named['side_0'] in fset or named['side_1'] in fset
|
|
or named['spike'] in fset or named['merged'] in fset)
|
|
|
|
|
|
def test_one(D):
|
|
D.is_planar(set_embedding=True)
|
|
residual_colourings = []
|
|
other_bad = []
|
|
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
|
|
# Compute the n_k sequence for this reduction
|
|
# n_k = degree of the face F_k of original G' = length of
|
|
# the corresponding face in the reduced dual after subtracting
|
|
# the reduction effect.
|
|
# Simpler: the flank face F^♭_{i, i+1} has length n_i - 1.
|
|
# So n_i = length of F^♭_{i,i+1} + 1.
|
|
# Get all faces:
|
|
named_face_lengths = {}
|
|
for f in H.faces():
|
|
fset = {frozenset(e) for e in f}
|
|
if named['side_0'] in fset and named['spike'] in fset:
|
|
named_face_lengths['flank_lower'] = len(f)
|
|
if named['spike'] in fset and named['side_1'] in fset:
|
|
named_face_lengths['flank_upper'] = len(f)
|
|
if (named['side_0'] in fset and named['side_1'] in fset
|
|
and named['merged'] in fset):
|
|
named_face_lengths['outer'] = len(f)
|
|
if (named['merged'] in fset and named['side_0'] not in fset
|
|
and named['side_1'] not in fset
|
|
and named['spike'] not in fset):
|
|
named_face_lengths['merged'] = len(f)
|
|
n_i = named_face_lengths.get('flank_lower', 0) + 1 # = n_i, where the flank covers
|
|
n_ip1 = named_face_lengths.get('flank_upper', 0) + 1
|
|
# n_{i+3} = F_merged length + 2
|
|
n_ip3 = named_face_lengths.get('merged', 0) + 2
|
|
# n_{i+2} + n_{i+4} from outer
|
|
outer_len = named_face_lengths.get('outer', 0)
|
|
# outer_len = n_{i+2} + n_{i+4} - 3
|
|
|
|
for col in cand:
|
|
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 colouring
|
|
# Count G'-pentagons total and hit
|
|
p_total = 0
|
|
p_hit = 0
|
|
non_pent_uncovered = []
|
|
for f in H.faces():
|
|
if not is_g_prime_face(f, named): continue
|
|
L = len(f)
|
|
verts = {u for (u, v) in f} | {v for (u, v) in f}
|
|
if L == 5:
|
|
p_total += 1
|
|
if verts & S: p_hit += 1
|
|
else:
|
|
if L % 3 != 0 and verts.issubset(V_union):
|
|
non_pent_uncovered.append(L)
|
|
# Is this a "residual" case?
|
|
S_size = len(S)
|
|
if S_size == 8 and p_total == p_hit:
|
|
residual_colourings.append({
|
|
'n_i': n_i, 'n_ip1': n_ip1, 'n_ip3': n_ip3,
|
|
'outer_len': outer_len,
|
|
'p_total': p_total, 'p_hit': p_hit,
|
|
'S_size': S_size,
|
|
'non_pent_uncovered': non_pent_uncovered,
|
|
})
|
|
elif S_size == 8 and p_hit == p_total - 1 and p_total >= 7:
|
|
# Border case: only 1 pentagon uncovered
|
|
pass
|
|
|
|
return residual_colourings
|
|
|
|
|
|
def main(max_n=20, time_budget_per_n=1800):
|
|
print("Detailed analysis of |S|=8, p_hit = p_total residual "
|
|
"chord-apex+Kempe colourings.\n")
|
|
grand_residuals = []
|
|
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_count = 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)
|
|
resids = test_one(D)
|
|
for r in resids:
|
|
r['n_G'] = n
|
|
r['tri_idx'] = tri_idx
|
|
n_count += len(resids)
|
|
grand_residuals.extend(resids)
|
|
elapsed = time.time() - start
|
|
print(f"n={n}: {n_count} residual colourings [{elapsed:.0f}s]")
|
|
sys.stdout.flush()
|
|
print()
|
|
print("=" * 70)
|
|
print(f"Total residual colourings: {len(grand_residuals)}")
|
|
if grand_residuals:
|
|
# n_k sequence distribution
|
|
seq_dist = {}
|
|
for r in grand_residuals:
|
|
key = (r['n_i'], r['n_ip1'], r['n_ip3'], r['outer_len'])
|
|
seq_dist[key] = seq_dist.get(key, 0) + 1
|
|
print("\n(n_i, n_{i+1}, n_{i+3}, F_outer length) distribution:")
|
|
for k, c in sorted(seq_dist.items(), key=lambda x: -x[1]):
|
|
print(f" {k}: {c}")
|
|
# For each, does a non-pentagon G'-face provide a deciding face?
|
|
has_non_pent = sum(1 for r in grand_residuals if r['non_pent_uncovered'])
|
|
print(f"\nResidual colourings with at least one length-≢0-mod-3 "
|
|
f"G'-face uncovered: {has_non_pent} / {len(grand_residuals)} "
|
|
f"({100*has_non_pent/len(grand_residuals):.2f}%)")
|
|
# Lengths of those non-pent G'-faces
|
|
len_dist = {}
|
|
for r in grand_residuals:
|
|
for L in r['non_pent_uncovered']:
|
|
len_dist[L] = len_dist.get(L, 0) + 1
|
|
print("Lengths of uncovered non-pentagon G'-faces:")
|
|
for L, c in sorted(len_dist.items()):
|
|
print(f" |f| = {L}: {c}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|