face_monochromatic_pairs: cycle-side splits and shared-vertex transitions
For each chord-apex+Kempe colouring (n in [12, 18]), record:
(1) (#L, #R) split of c-edge sides along K_b and K_c. #L == #R only
in 35.43% of colourings (the rest have unbalanced sides --
consistent with the empirical Heawood non-constancy).
(2) Ordered sequence of (i_b mod 2, i_c mod 2) parity pairs at
shared K_b cap K_c vertices in K_b walk order, plus a tally of
transitions in the 4-state space.
Two clean structural observations on the transition matrix:
(A) i_b parity strictly alternates between consecutive shared
K_b-vertices. Every transition goes (0, *) -> (1, *) or
(1, *) -> (0, *); transitions within (0, *) or within (1, *) are
never observed. So shared positions on K_b alternate even/odd in
walk order -- the gap on K_b between consecutive shared vertices
is always odd.
(B) From odd-i_b states, i_c parity must flip too: (1, 0) only
transitions to (0, 1) and (1, 1) only to (0, 0). From even-i_b
states, both i_c outcomes occur.
(B) is explained structurally: at an odd-i_b shared vertex K_b leaves
via the a-edge (which is also on K_c), so K_b and K_c traverse the
same edge and K_c advances exactly one step, flipping i_c. At an
even-i_b shared vertex K_b leaves via the b-edge (off K_c), so K_c
advances at its own pace and i_c can be either parity at the next
shared vertex.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
|||||||
|
"""Two diagnostics:
|
||||||
|
|
||||||
|
(1) Cycle-closure / winding count on K_b and K_c.
|
||||||
|
For each cycle, count the c-edges (off-cycle) at K-vertices that
|
||||||
|
lie on the local LEFT vs local RIGHT side of the walking direction.
|
||||||
|
Under constancy (h constant on V(K_b) U V(K_c)), Lemma A predicts
|
||||||
|
sides alternate, so #LEFT == #RIGHT == |V(K)|/2 exactly. We tally
|
||||||
|
the empirical (#LEFT, #RIGHT) split for K_b and K_c.
|
||||||
|
|
||||||
|
(2) Ordered sequence of (i_b mod 2, i_c mod 2) parity pairs at shared
|
||||||
|
vertices, sorted by i_b position. We look at the sequence of
|
||||||
|
transitions in the 4-state space {(0,0), (0,1), (1,0), (1,1)},
|
||||||
|
and ask whether the empirical sequences are constrained.
|
||||||
|
|
||||||
|
Run with: sage experiments/check_shared_sequence_and_winding.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,
|
||||||
|
trace_kempe_cycle,
|
||||||
|
edge_idx,
|
||||||
|
)
|
||||||
|
from check_heawood_on_kempe import dual_of, heawood_numbers
|
||||||
|
from check_heawood_local_side import c_edge_local_side
|
||||||
|
|
||||||
|
|
||||||
|
def walk_positions(walk):
|
||||||
|
pos = {}
|
||||||
|
for k, (_, leave_v) in enumerate(walk):
|
||||||
|
if leave_v not in pos:
|
||||||
|
pos[leave_v] = k
|
||||||
|
return pos
|
||||||
|
|
||||||
|
|
||||||
|
def collect_cycle_sides(H, walk, edges, col, emb, third_color):
|
||||||
|
"""For each step k of walk, compute side ('L' or 'R') of the
|
||||||
|
third_color edge at the leave_v vertex relative to the walking
|
||||||
|
direction (in_edge = walk[k], out_edge = walk[k+1]).
|
||||||
|
Returns list of sides indexed by k.
|
||||||
|
"""
|
||||||
|
L = len(walk)
|
||||||
|
sides = []
|
||||||
|
for k in range(L):
|
||||||
|
v = walk[k][1]
|
||||||
|
in_e = edges[walk[k][0]]
|
||||||
|
out_e = edges[walk[(k + 1) % L][0]]
|
||||||
|
u_in = in_e[0] if in_e[1] == v else in_e[1]
|
||||||
|
u_out = out_e[0] if out_e[1] == v else out_e[1]
|
||||||
|
s = c_edge_local_side(v, third_color, col, edges, emb, u_in, u_out)
|
||||||
|
sides.append(s)
|
||||||
|
return sides
|
||||||
|
|
||||||
|
|
||||||
|
def test_one(D):
|
||||||
|
D.is_planar(set_embedding=True)
|
||||||
|
n_col = 0
|
||||||
|
# (1) Side-split distribution: (#L, #R, L_cycle).
|
||||||
|
kb_split = {}; kc_split = {}
|
||||||
|
# (2) Ordered sequence of (i_b mod 2, i_c mod 2) at shared verts in
|
||||||
|
# K_b walk order: sequences (as tuples).
|
||||||
|
seq_dist = {} # signature tuple -> count
|
||||||
|
# Aggregate transition counts between the 4 parity states
|
||||||
|
transitions = {} # (prev_state, curr_state) -> 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)
|
||||||
|
emb = H.get_embedding()
|
||||||
|
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]
|
||||||
|
b_color, c_color = bs[0], bs[1]
|
||||||
|
walk_b = trace_kempe_cycle(edges, col, merged_idx, (a, b_color))
|
||||||
|
walk_c = trace_kempe_cycle(edges, col, merged_idx, (a, c_color))
|
||||||
|
pos_b = walk_positions(walk_b)
|
||||||
|
pos_c = walk_positions(walk_c)
|
||||||
|
shared = set(pos_b.keys()) & set(pos_c.keys())
|
||||||
|
# (1)
|
||||||
|
sides_b = collect_cycle_sides(H, walk_b, edges, col, emb, c_color)
|
||||||
|
sides_c = collect_cycle_sides(H, walk_c, edges, col, emb, b_color)
|
||||||
|
Lb_len = len(walk_b); Lc_len = len(walk_c)
|
||||||
|
Lb = sum(1 for s in sides_b if s == 'L')
|
||||||
|
Rb = sum(1 for s in sides_b if s == 'R')
|
||||||
|
Lc = sum(1 for s in sides_c if s == 'L')
|
||||||
|
Rc = sum(1 for s in sides_c if s == 'R')
|
||||||
|
kb_split[(Lb, Rb)] = kb_split.get((Lb, Rb), 0) + 1
|
||||||
|
kc_split[(Lc, Rc)] = kc_split.get((Lc, Rc), 0) + 1
|
||||||
|
# (2)
|
||||||
|
seq = []
|
||||||
|
for k in range(Lb_len):
|
||||||
|
v = walk_b[k][1]
|
||||||
|
if v in shared:
|
||||||
|
seq.append((k % 2, pos_c[v] % 2))
|
||||||
|
# Signature: count occurrences of each state in order.
|
||||||
|
sig = tuple(seq)
|
||||||
|
seq_dist[sig] = seq_dist.get(sig, 0) + 1
|
||||||
|
# Transitions:
|
||||||
|
for i in range(len(seq)):
|
||||||
|
prev = seq[i - 1] # wraps to seq[-1]
|
||||||
|
curr = seq[i]
|
||||||
|
transitions[(prev, curr)] = transitions.get(
|
||||||
|
(prev, curr), 0) + 1
|
||||||
|
return n_col, kb_split, kc_split, seq_dist, transitions
|
||||||
|
|
||||||
|
|
||||||
|
def main(max_n=18, time_budget_per_n=1800):
|
||||||
|
print(f"Cycle-side splits and shared-vertex sequence, "
|
||||||
|
f"n in [12, {max_n}]\n")
|
||||||
|
grand_col = 0
|
||||||
|
grand_kb = {}; grand_kc = {}
|
||||||
|
grand_seq = {}
|
||||||
|
grand_tr = {}
|
||||||
|
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
|
||||||
|
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, kb_i, kc_i, seq_i, tr_i = test_one(D)
|
||||||
|
n_col_n += ni
|
||||||
|
for k, v in kb_i.items(): grand_kb[k] = grand_kb.get(k, 0) + v
|
||||||
|
for k, v in kc_i.items(): grand_kc[k] = grand_kc.get(k, 0) + v
|
||||||
|
for k, v in seq_i.items(): grand_seq[k] = grand_seq.get(k, 0) + v
|
||||||
|
for k, v in tr_i.items(): grand_tr[k] = grand_tr.get(k, 0) + v
|
||||||
|
elapsed = time.time() - start
|
||||||
|
print(f"n={n}: {n_col_n} col., [{elapsed:.0f}s]")
|
||||||
|
sys.stdout.flush()
|
||||||
|
grand_col += n_col_n
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"Grand totals (n in [12, {max_n}], {grand_col} colourings)")
|
||||||
|
|
||||||
|
print(f"\n K_b (#L, #R) distribution:")
|
||||||
|
for k, v in sorted(grand_kb.items()):
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
print(f"\n K_c (#L, #R) distribution:")
|
||||||
|
for k, v in sorted(grand_kc.items()):
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
# Check: is #L == #R always?
|
||||||
|
kb_equal = sum(v for (L, R), v in grand_kb.items() if L == R)
|
||||||
|
kc_equal = sum(v for (L, R), v in grand_kc.items() if L == R)
|
||||||
|
total_kb = sum(grand_kb.values())
|
||||||
|
total_kc = sum(grand_kc.values())
|
||||||
|
print(f"\n K_b: #L == #R in {kb_equal}/{total_kb} "
|
||||||
|
f"({100*kb_equal/max(1,total_kb):.2f}%)")
|
||||||
|
print(f" K_c: #L == #R in {kc_equal}/{total_kc} "
|
||||||
|
f"({100*kc_equal/max(1,total_kc):.2f}%)")
|
||||||
|
|
||||||
|
print(f"\n Transition counts in the 4-state parity space "
|
||||||
|
f"((prev) -> (curr)):")
|
||||||
|
states = [(0,0), (0,1), (1,0), (1,1)]
|
||||||
|
for s1 in states:
|
||||||
|
for s2 in states:
|
||||||
|
c = grand_tr.get((s1, s2), 0)
|
||||||
|
print(f" {s1} -> {s2}: {c}")
|
||||||
|
|
||||||
|
print(f"\n Top 10 most common shared-sequence signatures (by K_b order):")
|
||||||
|
top = sorted(grand_seq.items(), key=lambda kv: -kv[1])[:10]
|
||||||
|
for sig, c in top:
|
||||||
|
print(f" {sig}: {c}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user