diff --git a/papers/face_monochromatic_pairs/experiments/check_combinatorial_winding.py b/papers/face_monochromatic_pairs/experiments/check_combinatorial_winding.py new file mode 100644 index 0000000..c98c601 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_combinatorial_winding.py @@ -0,0 +1,166 @@ +"""Verify the combinatorial winding invariant for simple closed cycles +in a 3-regular planar graph: + + Claim: For a simple closed cycle C in a 3-regular planar graph H + (with C ⊆ E(H), |C|/2 edges of C at each vertex of C), walked CCW + around its interior, define for each v ∈ V(C): + turn-sign(v) = +1 if the third edge (= the one of v's 3 edges NOT + on C) is on the OUTSIDE of C, locally at v. + (= the walker turns LEFT into the interior). + -1 if the third edge is on the INSIDE. + + Hypothesis: Σ_v turn-sign(v) = ±6 (= ±(L_C / |V|_C-cycle-curvature)). + +This is a combinatorial Gauss-Bonnet for cubic plane graphs: +internal/external turn parity around any face / cycle. + +Sanity-check this on: + - Triangle (length 3) in K_4. + - Quadrilateral (length 4) in Q_3. + - Pentagon (length 5) in dodecahedron. + - Hexagon (length 6) in some triangulation dual. + +Also on Kempe cycles in chord-apex+Kempe colourings of reduced duals. + +If the invariant holds (always ±6), then combining with Lemma 5.2's +alternation (= turn signs alternate +,-,+,- on any K_b under constancy), +we'd have #(+) = #(-) = L_b / 2, so #(+) - #(-) = 0 ≠ ±6, giving a +direct contradiction. + +Run with: sage experiments/check_combinatorial_winding.py +""" +import os +import sys + +from sage.all import Graph +from sage.graphs.graph_generators import graphs + + +def cycle_winding(G, cycle_vertices): + """Compute Σ_v turn-sign(v) for a simple closed cycle in 3-reg planar G. + + cycle_vertices: list of vertices in walking order around the cycle. + Walking direction: CCW (= traversal such that interior is on the LEFT). + + For each v in the cycle, the 3 edges at v are split as: 2 cycle + edges (to v's neighbours in the cycle) and 1 third edge (off-cycle). + The third edge is at some position in v's CW rotation system. + + turn-sign(v) = +1 if the third edge is OUTSIDE the cycle (= on the + right of the CCW walk locally), -1 if INSIDE. + """ + G.is_planar(set_embedding=True) + emb = G.get_embedding() + L = len(cycle_vertices) + total = 0 + for k in range(L): + v = cycle_vertices[k] + prev = cycle_vertices[(k - 1) % L] + nxt = cycle_vertices[(k + 1) % L] + # CW rotation at v: emb[v] is the list of neighbours in CW + # order. + nbrs = emb[v] + # We need positions of prev, nxt, and third. + if prev not in nbrs or nxt not in nbrs: + return None + # The third edge endpoint: + third = [u for u in nbrs if u != prev and u != nxt] + if len(third) != 1: + return None + third = third[0] + # CW order of (prev, nxt, third) in emb[v]: + idx_prev = nbrs.index(prev) + idx_nxt = nbrs.index(nxt) + idx_third = nbrs.index(third) + # In CW order, what's the cyclic arrangement? + # Walking CCW around the cycle means walker is on edge (prev, v), + # at v, and exits via (v, nxt). The interior of the cycle is + # on the LEFT of the walker. + # The third edge is at idx_third in CW rotation. + # If we go CW from idx_prev to idx_nxt: + # - if idx_third is in this CW interval, third is on one side; + # - else on the other side. + # For 3-regular vertex with CW order (positions 0, 1, 2 cyclically), + # idx_prev, idx_nxt, idx_third are some permutation of {0, 1, 2}. + # Define: third is on the RIGHT of walker (= going CW from prev + # to nxt without passing through third) ⟺ third is NOT between + # prev and nxt in CW direction. + # The CW order at v: nbrs[0], nbrs[1], nbrs[2]. + # CW direction at v: 0 → 1 → 2 → 0. + # CW from prev to nxt: starting at idx_prev, going CW (= +1 mod 3), + # we reach idx_nxt after some steps. + # If we pass through idx_third in 1 step: third is between prev + # and nxt in CW direction (i.e., on one specific side). + # If we don't pass through idx_third (= go directly from prev to + # nxt in 1 CW step): third is on the OTHER side. + # 1 CW step from idx_prev: (idx_prev + 1) % 3. + if (idx_prev + 1) % 3 == idx_nxt: + # CW step from prev to nxt is direct (1 step). + # Third is at the "other" position, NOT between in CW. + # This means third is on the "left" of walker going CCW + # along the cycle (= interior side). + # turn-sign = -1 (third inside). + sign = -1 + elif (idx_prev + 2) % 3 == idx_nxt: + # CW step from prev to nxt is 2 steps (passing through + # idx_third). + # Third is BETWEEN prev and nxt in CW direction. + # turn-sign = +1 (third outside, walker turns left into + # interior). + sign = +1 + else: + return None + total += sign + return total + + +def face_to_cycle(face): + """Convert face (= list of edges) to ordered vertex cycle.""" + if not face: + return None + verts = [face[0][0]] + for e in face: + verts.append(e[1]) + return verts[:-1] # last vertex = first vertex + + +def main(): + tests = [] + # K_4 = tetrahedron + K4 = graphs.CompleteGraph(4) + K4.is_planar(set_embedding=True) + tests.append(('K_4', K4)) + # Q_3 = cube + Q3 = graphs.CubeGraph(3) + Q3.is_planar(set_embedding=True) + tests.append(('Q_3', Q3)) + # Dodecahedron + Dod = graphs.DodecahedralGraph() + Dod.is_planar(set_embedding=True) + tests.append(('Dodecahedron', Dod)) + # Triangular prism + Tprism = graphs.CompleteBipartiteGraph(2, 3) # not 3-regular + # Use Q3 plus more + tests.append(('Triangular prism (3-prism)', graphs.GeneralizedPetersenGraph(3, 1))) + + for name, G in tests: + if not all(G.degree(v) == 3 for v in G.vertex_iterator()): + print(f"\n{name}: NOT 3-regular (degree set = " + f"{sorted(set(G.degree()))}); skipping") + continue + if not G.is_planar(set_embedding=True): + print(f"\n{name}: NOT planar; skipping") + continue + print(f"\n{name}: |V|={G.order()}, |E|={G.size()}, faces:") + faces = G.faces() + for f in faces: + cyc = face_to_cycle(f) + if cyc is None: + print(f" bad face") + continue + w = cycle_winding(G, cyc) + print(f" face length {len(cyc)}, cycle={cyc}, winding={w}") + + +if __name__ == '__main__': + main()