From 5552e078037a327e279e343380a14679b59d908c Mon Sep 17 00:00:00 2001 From: didericis Date: Wed, 17 Jun 2026 21:43:21 -0400 Subject: [PATCH] Add force-first Heawood labelling to the medial tire dual-cut experiment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit heawood_labelling(): depth-seeded force-first +/-1 labelling of the source-dual cut, targeting sum ≡ 0 (mod 3) on each dual face (vertex link), with bookkeeping for seeds, forced fills, unforceable faces, and failing faces. Co-Authored-By: Claude Opus 4.8 --- .../medial_tire_dual_cut_experiment.py | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py index b4583f3..df94785 100644 --- a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py +++ b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py @@ -259,6 +259,155 @@ def dual_cut_distances(result): return nx.single_source_shortest_path_length(dual_cut_graph(result), root), root +def _dual_faces_by_vertex(result): + """Each *dual face* of the source-dual cut is the link of a primal vertex + ``v``: the set of triangular faces (dual vertices) incident to ``v``, of + size ``deg(v)``. Returns ``{v: [face index, ...]}``.""" + faces = result["faces"] + face_of = defaultdict(list) + for fi, f in enumerate(faces): + for v in f: + face_of[v].append(fi) + return dict(face_of) + + +def heawood_labelling(result): + """Force-first Heawood labelling of the source-dual cut, seeded by walk depth. + + Each dual vertex (triangular face of the source graph) is given a label in + ``{+1, -1}``; the target is that every *dual face* -- the link of a primal + vertex ``v``, i.e. the ``deg(v)`` triangles around ``v`` -- has label sum + ``≡ 0 (mod 3)`` (Heawood's condition, whose existence is equivalent to + 4-colourability of the source triangulation). + + The procedure interleaves forcing and depth-seeding: + + 1. **Force (saturate):** while some dual face has **two or fewer** + unlabelled vertices, fill them so that face's sum is ``≡ 0 (mod 3)``. + + * one unlabelled slot ``t``: forced to the unique ``±1`` with + ``label(t) ≡ -S (mod 3)`` over the known sum ``S``. If ``-S ≡ 0`` + no ``±1`` completes the face -- it is an *unforceable* face, + recorded as a violation (the slot is still filled by its own depth + parity so the walk can continue). + * two unlabelled slots: forced to the same sign when the rule + demands it (both ``-1`` or both ``+1``); when the rule only demands + *opposite* signs (the known labels already sum to ``≡ 0``), each + slot takes its own depth parity -- the even-depth one ``+1``, the + odd-depth one ``-1``. + + Repeat until no dual face has ``≤ 2`` unlabelled vertices. + + 2. **Seed:** take the unlabelled dual vertex of smallest walk depth (none + of whose incident faces is forcing it) and label it by depth parity + (``+1`` if even, ``-1`` if odd). Return to step 1. + + Forcing only ever fills *empty* slots, so no label is overwritten and the + only failure mode is a dual face whose final label sum is ``≢ 0 (mod 3)``. + + Returns a dict with ``labels`` (``{dual vertex: ±1}``), ``failing_faces`` + (primal vertices whose final sum is ``≢ 0 mod 3``), ``success`` (no failing + faces), ``seeds`` (depth-seeded vertices, step 2), ``forced`` (vertices + filled by step 1, split into ``single``/``pair_same``/``pair_opposite``), + ``unforceable`` (one-slot faces with no valid ``±1``), and bookkeeping + (``n_dual``, ``max_depth``).""" + G = result["G"] + dist, root = dual_cut_distances(result) + if root is None: + raise ValueError("source-dual cut has no entry down tooth to root from") + face_of = {v: face_of_v + for v, face_of_v in sorted(_dual_faces_by_vertex(result).items())} + + def parity(t): + return 1 if dist[t] % 2 == 0 else -1 + + labels = {} + counts = {"single": 0, "pair_same": 0, "pair_opposite": 0} + unforceable = [] + seeds = 0 + + def saturate(): + changed = True + while changed: + changed = False + for v, tri in face_of.items(): + unl = [t for t in tri if t not in labels] + s = sum(labels[t] for t in tri if t in labels) % 3 + needed = (-s) % 3 # value (or pair-sum) needed mod 3 + if len(unl) == 1: + t = unl[0] + if needed == 1: + labels[t] = 1 + elif needed == 2: + labels[t] = -1 + else: # -S ≡ 0: no ±1 closes this face + unforceable.append(v) + labels[t] = parity(t) + counts["single"] += 1 + changed = True + elif len(unl) == 2: + t1, t2 = unl + if needed == 1: + labels[t1] = labels[t2] = -1 + counts["pair_same"] += 1 + elif needed == 2: + labels[t1] = labels[t2] = 1 + counts["pair_same"] += 1 + else: # opposite signs: each by its depth parity + labels[t1], labels[t2] = parity(t1), parity(t2) + counts["pair_opposite"] += 1 + changed = True + + order = sorted(dist, key=lambda t: (dist[t], t)) + saturate() + while len(labels) < len(dist): + u = next(t for t in order if t not in labels) + labels[u] = parity(u) + seeds += 1 + saturate() + + failing_faces = [] + for v, tri in face_of.items(): + s = sum(labels[t] for t in tri) + if s % 3 != 0: + failing_faces.append({ + "vertex": v, "degree": G.degree(v), "sum": s, + "labels": [labels[t] for t in tri]}) + + return { + "labels": labels, "failing_faces": failing_faces, + "seeds": seeds, "forced": counts, "unforceable": unforceable, + "root": root, "n_dual": len(result["faces"]), + "n_labelled": len(labels), + "max_depth": max(dist.values()) if dist else 0, + "success": not failing_faces, + } + + +def heawood_report(result): + """One-line-per-fact summary of ``heawood_labelling`` for printing.""" + h = heawood_labelling(result) + verdict = ("TERMINATES successfully (every dual face sum ≡ 0 mod 3)" + if h["success"] else + "FAILS (a dual face has label sum ≢ 0 mod 3)") + f = h["forced"] + lines = [ + f"Heawood labelling: {verdict}", + f" dual vertices: {h['n_dual']} labelled: {h['n_labelled']} " + f"max walk depth: {h['max_depth']}", + f" depth-seeded (step 2): {h['seeds']} forced (step 1): " + f"{f['single']} single + {f['pair_same']} same-sign pairs + " + f"{f['pair_opposite']} opposite-sign pairs", + f" unforceable faces (1 slot, no valid ±1): {len(h['unforceable'])}", + f" failing dual faces (sum % 3 != 0): {len(h['failing_faces'])}", + ] + for ff in h["failing_faces"]: + lines.append( + f" vertex {ff['vertex']} (deg {ff['degree']}): " + f"sum {ff['sum']} from labels {ff['labels']}") + return "\n".join(lines) + + # --------------------------------------------------------------------------- # # The four chained entry points. # --------------------------------------------------------------------------- # @@ -881,6 +1030,9 @@ def main(): help="render tread 0 (the source cap) to PNG") parser.add_argument("--pdf", metavar="PATH", help="render dual, tire tree, and tire cuts in one PDF") + parser.add_argument("--heawood", action="store_true", + help="run the walk-depth-seeded Heawood labelling and " + "report whether it terminates successfully") args = parser.parse_args() rng = random.Random(args.seed) @@ -912,6 +1064,8 @@ def main(): min_degree=args.min_degree) print(summary(result)) + if args.heawood: + print(heawood_report(result)) if args.png: draw_png(result, args.png) print(f"wrote {args.png}")