Add even-level-cycle colouring program harness
Constructive route: surger G so every level cycle is even (two-vertex leaf gadget
on terminal triangles -> 4-wheel, no defect; diamond on odd internal seams), take
the canonical even colouring of M(G') (no 4CT used), Kempe-remove the planted
degree-4/3 vertices to reach a proper 3-colouring of M(G).
Pipeline runs end to end on synthetic ring triangulations: surgery, canonical
colouring, and gadget removal all work; the program lands on the CYCLE LAYER
(39/60 ok, rest fail:diamond-switch). Diagnostic: a descendable colouring always
EXISTS (M(G) is 3-colourable), so failures are Kempe-reachability from the
canonical even colouring, not non-existence -- the entire difficulty is localised
there. Greedy per-diamond switching is insufficient because diamonds share vertical
{1,3}-Kempe cycles; the principled solve is joint (bipartiteness of the diamond /
side-cycle constraint graph), which is the identified next step. Includes the leaf
gadget figure and a findings note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
"""Picture: evening a terminal (leaf) triangle by the two-vertex operation:
|
||||
add y at the midpoint of uv and z at the centroid of uvt, delete uv, add edges
|
||||
xy, uy, vy, zy, zu, zv, zt. The leaf becomes a 4-wheel tread with hub z.
|
||||
|
||||
Panels:
|
||||
A before: terminal face uvt, level cycle C_k = the 3-cycle (odd seam)
|
||||
B after: seam u-y-v-t (length 4, even); leaf = 4-wheel with hub z (level k+1)
|
||||
C medial overlay with the canonical colouring: seam apexes mono-3,
|
||||
leaf annular 4-cycle alternating 1,2 -- proper, no ears, no defect.
|
||||
"""
|
||||
|
||||
import os
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PAL = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # colours "1","2","3"
|
||||
GRAY = "#999999"
|
||||
|
||||
u, v, t, x = (-1.0, 0.0), (1.0, 0.0), (0.0, -1.6), (0.0, 1.0)
|
||||
y = (0.0, 0.0) # midpoint of uv
|
||||
z = (0.0, -1.6 / 3) # centroid of uvt
|
||||
|
||||
|
||||
def mid(a, b):
|
||||
return ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
||||
|
||||
|
||||
def vertex(ax, p, name, dx, dy, color="black"):
|
||||
ax.plot(*p, "o", color=color, ms=5.5, zorder=6)
|
||||
ax.annotate(name, p, textcoords="offset points", xytext=(dx, dy),
|
||||
fontsize=10, fontweight="bold", zorder=6)
|
||||
|
||||
|
||||
def panel_before(ax):
|
||||
ax.fill([u[0], v[0], t[0]], [u[1], v[1], t[1]], color="#fde9d9")
|
||||
for a, b in [(u, v), (v, t), (t, u)]:
|
||||
ax.plot([a[0], b[0]], [a[1], b[1]], color="black", lw=2.4)
|
||||
for p in (u, v):
|
||||
ax.plot([x[0], p[0]], [x[1], p[1]], color=GRAY, lw=0.9)
|
||||
ax.annotate("terminal face\n(leaf of tire tree)", (0, -0.62), ha="center",
|
||||
fontsize=8, color="#b06030")
|
||||
vertex(ax, u, "u", -12, -2); vertex(ax, v, "v", 8, -2)
|
||||
vertex(ax, t, "t", 0, -14); vertex(ax, x, "x", 8, 2)
|
||||
ax.annotate("(apex in tread above)", x, textcoords="offset points",
|
||||
xytext=(16, -2), fontsize=7, color=GRAY)
|
||||
|
||||
|
||||
def panel_after(ax, faint=1.0):
|
||||
# wheel faces
|
||||
for tri, c in [((u, y, z), "#fde9d9"), ((y, v, z), "#fdf3d9"),
|
||||
((v, t, z), "#fde9d9"), ((t, u, z), "#fdf3d9")]:
|
||||
ax.fill([p[0] for p in tri], [p[1] for p in tri], color=c, alpha=faint)
|
||||
# seam (level cycle) bold: u-y, y-v, v-t, t-u
|
||||
for a, b in [(u, y), (y, v), (v, t), (t, u)]:
|
||||
ax.plot([a[0], b[0]], [a[1], b[1]], color="black", lw=2.4, alpha=faint)
|
||||
# parent spokes xu, xy, xv
|
||||
for p in (u, y, v):
|
||||
ax.plot([x[0], p[0]], [x[1], p[1]], color=GRAY, lw=0.9, alpha=faint)
|
||||
# leaf spokes zu, zy, zv, zt
|
||||
for p in (u, y, v, t):
|
||||
ax.plot([z[0], p[0]], [z[1], p[1]], color="black", lw=1.0, alpha=faint)
|
||||
vertex(ax, u, "u", -12, -2); vertex(ax, v, "v", 8, -2)
|
||||
vertex(ax, t, "t", 0, -14); vertex(ax, x, "x", 8, 2)
|
||||
vertex(ax, y, "y", 6, 6); vertex(ax, z, "z", 7, -4)
|
||||
|
||||
|
||||
def panel_medial(ax):
|
||||
panel_after(ax, faint=0.35)
|
||||
apexes = {"uy": mid(u, y), "yv": mid(y, v), "vt": mid(v, t), "tu": mid(t, u)}
|
||||
leaf_ann = {"zu": mid(z, u), "zy": mid(z, y), "zv": mid(z, v), "zt": mid(z, t)}
|
||||
par_ann = {"ux": mid(u, x), "xy": mid(x, y), "xv": mid(x, v)}
|
||||
# leaf annular 4-cycle (faces uyz, yvz, vtz, tuz)
|
||||
ring = ["zu", "zy", "zv", "zt"]
|
||||
for i in range(4):
|
||||
a, b = leaf_ann[ring[i]], leaf_ann[ring[(i + 1) % 4]]
|
||||
ax.plot([a[0], b[0]], [a[1], b[1]], color="#555555", lw=1.6, zorder=4)
|
||||
# parent annular path m_ux - m_xy - m_xv (faces xuy, xyv)
|
||||
for a, b in [("ux", "xy"), ("xy", "xv")]:
|
||||
ax.plot([par_ann[a][0], par_ann[b][0]], [par_ann[a][1], par_ann[b][1]],
|
||||
color="#555555", lw=1.6, zorder=4)
|
||||
# apex spokes: each seam apex to its two leaf-annular and two parent nbrs
|
||||
spokes = [("uy", "zu"), ("uy", "zy"), ("yv", "zy"), ("yv", "zv"),
|
||||
("vt", "zv"), ("vt", "zt"), ("tu", "zt"), ("tu", "zu")]
|
||||
for a, b in spokes:
|
||||
pa, pb = apexes[a], leaf_ann[b]
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#aaaaaa", lw=1.0, zorder=3)
|
||||
for a, b in [("uy", "ux"), ("uy", "xy"), ("yv", "xy"), ("yv", "xv")]:
|
||||
pa, pb = apexes[a], par_ann[b]
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#aaaaaa", lw=1.0, zorder=3)
|
||||
# off-picture parent stubs for m_vt, m_tu
|
||||
for k, d in [("vt", (0.25, -0.18)), ("tu", (-0.25, -0.18))]:
|
||||
p = apexes[k]
|
||||
ax.plot([p[0], p[0] + d[0]], [p[1], p[1] + d[1]], color="#cccccc",
|
||||
lw=0.9, linestyle=":", zorder=2)
|
||||
# colours: apexes mono-3 (green); leaf ring alternating 1,2; parent 1,2,1
|
||||
col = {}
|
||||
for k in apexes: col[("a", k)] = 2
|
||||
for k, c in zip(ring, (0, 1, 0, 1)): col[("l", k)] = c
|
||||
for k, c in zip(("ux", "xy", "xv"), (0, 1, 0)): col[("p", k)] = c
|
||||
for k, p in apexes.items():
|
||||
ax.plot(*p, "o", color=PAL[2], ms=11, markeredgecolor="black", zorder=5)
|
||||
for k, p in leaf_ann.items():
|
||||
ax.plot(*p, "o", color=PAL[col[("l", k)]], ms=9,
|
||||
markeredgecolor="black", zorder=5)
|
||||
for k, p in par_ann.items():
|
||||
ax.plot(*p, "o", color=PAL[col[("p", k)]], ms=9,
|
||||
markeredgecolor="black", zorder=5)
|
||||
lbl = {"uy": (-26, 4), "yv": (12, 4), "vt": (12, -2), "tu": (-30, -2)}
|
||||
for k, p in apexes.items():
|
||||
ax.annotate(f"m_{k}", p, textcoords="offset points", xytext=lbl[k],
|
||||
fontsize=7, zorder=6)
|
||||
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(14, 4.8))
|
||||
for ax in axes:
|
||||
ax.set_xlim(-1.7, 1.7)
|
||||
ax.set_ylim(-2.1, 1.45)
|
||||
ax.set_aspect("equal")
|
||||
ax.axis("off")
|
||||
|
||||
panel_before(axes[0])
|
||||
axes[0].set_title("A. before: leaf = terminal face uvt\nseam C_k = 3-cycle (odd)",
|
||||
fontsize=9)
|
||||
panel_after(axes[1])
|
||||
axes[1].set_title("B. add y = mid(uv), z = centroid; delete uv;\n"
|
||||
"add xy, uy, vy, zy, zu, zv, zt\n"
|
||||
"seam u-y-v-t (even); leaf = 4-wheel, hub z (level k+1)",
|
||||
fontsize=9)
|
||||
panel_medial(axes[2])
|
||||
axes[2].set_title("C. medial + canonical colouring:\nseam apexes all 3 (green), "
|
||||
"leaf ring alternates 1,2 — proper, no ears", fontsize=9)
|
||||
|
||||
fig.suptitle(
|
||||
"Evening a terminal leaf with the two-vertex operation (y splits uv under x; "
|
||||
"z stellates the leaf as a wheel hub).\n"
|
||||
"Both new vertices have degree 4; the leaf tread is a 4-wheel with an even "
|
||||
"annular cycle, so the monochromatic-3 seam is VALID — no leaf defect.",
|
||||
fontsize=10)
|
||||
fig.tight_layout(rect=(0, 0, 1, 0.86))
|
||||
out = os.path.join(HERE, "evened_leaf.png")
|
||||
fig.savefig(out, dpi=170)
|
||||
print("wrote", out)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# The even-level-cycle colouring program
|
||||
|
||||
A constructive route distinct from the `R_T` composition line. Idea: surger a
|
||||
triangulation `G` so that **every level cycle is even**, take the resulting
|
||||
*canonical even colouring* of `M(G')` (no 4CT used), then **remove the planted
|
||||
vertices** by Kempe switches, landing on a proper 3-colouring of `M(G)` — i.e. a
|
||||
Tait/4CT colouring of `G`.
|
||||
|
||||
Scripts: `kempe_even_program_harness.py`, `draw_evened_leaf.py` (`evened_leaf.png`).
|
||||
|
||||
## The two surgeries
|
||||
|
||||
- **Leaf gadget (two vertices).** On a terminal triangle `uvt` with outer apex
|
||||
`x`: add `y = mid(uv)` and hub `z`; delete `uv`; add `xy, uy, vy, zy, zu, zv,
|
||||
zt`. Both new vertices have degree 4; the seam becomes `u-y-v-t` (even) and the
|
||||
leaf becomes a **4-wheel** with hub `z`. No ears, no chord — the monochromatic-3
|
||||
seam stays valid, so **leaves create no colouring defect**. (Earlier one-vertex
|
||||
chord version forced a `{0011,0101}` defect; this is strictly better.)
|
||||
- **Diamond.** On an odd internal seam edge `uv` with apexes `x,t`: delete `uv`,
|
||||
add `w ~ u,v,x,t` (degree 4). Flips that seam's parity.
|
||||
|
||||
By `n_T = p + Σq_i + 2b`, evening every internal seam makes every annular cycle
|
||||
even **except the root** (the outer triangle's odd charge `Σ_T n_T ≡ 3 (mod 2)` is
|
||||
invariant — confirmed; the root is handled as the one unavoidable defect region,
|
||||
solved by local backtracking).
|
||||
|
||||
## Canonical even colouring (constructive, no 4CT)
|
||||
|
||||
Every level-edge medial vertex → colour 3; every non-root annular cycle alternates
|
||||
1,2; the root region solved by DFS. Proper because each apex is forced 3 between
|
||||
two `{1,2}` pairs and (in the non-degenerate tread model) no two level edges are
|
||||
consecutive around a vertex or face.
|
||||
|
||||
## Removal conditions (degree-4 Kempe reduction — the historically *safe* case)
|
||||
|
||||
- **Diamond** `w` (quad `u-x-v-t`, restore diagonal `uv`): removable iff the pair
|
||||
`(m_ux,m_xv)` is distinct, `(m_vt,m_tu)` is distinct, ≤2 colours total; then
|
||||
`m_uv` takes the third.
|
||||
- **Gadget**: collapse `z` then `y` (or `y` then `z`), ending in a degree-3
|
||||
unstellation needing a rainbow triangle. Two orders = free choice.
|
||||
|
||||
## Status (synthetic ring triangulations, the clean-level-structure domain)
|
||||
|
||||
Pipeline runs end to end. Surgery, canonical colouring, and gadget removal all
|
||||
work. The program now lands squarely on the **cycle layer**:
|
||||
|
||||
```
|
||||
60 random ring triangulations: 39 ok, 21 fail:diamond-switch
|
||||
```
|
||||
|
||||
**Crucial diagnostic:** for a failing case, a simultaneously-removable proper
|
||||
3-colouring of `M(G')` was shown to **exist** (it must — `M(G)` is 3-colourable).
|
||||
So `fail:diamond-switch` is **not** non-existence; it is **Kempe-reachability** —
|
||||
whether switches carry the *canonical even* colouring to a descendable one. That is
|
||||
exactly the conjecture's core, and the harness has localised the entire program
|
||||
difficulty to it, with everything upstream constructive.
|
||||
|
||||
**Why greedy fails (and what's next).** Diamonds on different odd seams share
|
||||
*vertical* `{1,3}`-Kempe cycles, so per-diamond local switching cannot satisfy them
|
||||
simultaneously. The principled solve is joint: vertices = `{1,3}`-Kempe cycles,
|
||||
one edge per diamond joining its two side cycles; removability for all diamonds at
|
||||
once = a consistent XOR assignment = **bipartiteness** of that graph (no self-loop =
|
||||
the side cycles differ; no odd cycle = no three diamonds whose side cycles form a
|
||||
triangle). Insertion-site choice (which seam edge) and tread phase are the control
|
||||
knobs. Building this joint solver — and finding the smallest configuration, if any,
|
||||
forcing a self-loop or odd cycle — is the next step and the exact thing a proof
|
||||
would need to rule out.
|
||||
|
||||
## Caveats / domain
|
||||
|
||||
- Real plantri triangulations mostly `skip:chord-level-edge` under BFS-from-outer
|
||||
level structure — a reflection of how restrictive the clean nested-tire level
|
||||
structure is, not a harness bug. The synthetic concentric-ring generator produces
|
||||
the clean domain the program is stated for.
|
||||
- Root defect and the (deferred) outer-face handling are localised; the user has a
|
||||
separate idea for the outer face.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
+847
@@ -0,0 +1,847 @@
|
||||
"""Harness for the even-level-cycle colouring program.
|
||||
|
||||
Pipeline, per plane triangulation G (from plantri, sphere embedding, first
|
||||
face chosen as outer):
|
||||
|
||||
1. level structure from the outer triangle (BFS); seams (level cycles) per
|
||||
depth; classify faces; detect unsupported degeneracies (chords, flat
|
||||
faces that are not terminal triangles, malformed seams) and skip those.
|
||||
2. surgeries: the two-vertex LEAF GADGET on every terminal triangle face
|
||||
(add y on a seam edge under apex x, hub z at the face centre; delete uv;
|
||||
leaf becomes a 4-wheel) and a DIAMOND insertion on every remaining odd
|
||||
internal seam. All internal seams become even; by n_T = p + sum(q) + 2b
|
||||
every annular cycle is then even except the root tread's (the outer
|
||||
triangle's odd charge is invariant).
|
||||
3. canonical colouring of M(G'): every level-edge medial vertex (seam
|
||||
apexes and bite fins) gets colour 3; every non-root annular cycle
|
||||
alternates {1,2}; the root region (root annulus + the outer triangle's
|
||||
three mutually-adjacent medials) is solved by backtracking -- the one
|
||||
unavoidable defect.
|
||||
4. descent: remove the planted vertices one at a time. Diamonds collapse
|
||||
by the degree-4 condition (corner pairs distinct, <=2 colours, restored
|
||||
edge takes the third); the gadget collapses in two steps (either order),
|
||||
ending with a degree-3 unstellation needing a rainbow triangle. Before
|
||||
each step a bounded Kempe-switch search (components through the support,
|
||||
all three colour pairs, subsets of size <= 2) establishes the condition.
|
||||
5. verify the final colouring is a proper 3-colouring of M(G) -- which is a
|
||||
Tait/4CT colouring of the original triangulation.
|
||||
|
||||
Reports, per graph: ok / skip:<degeneracy> / fail:<stage>.
|
||||
|
||||
Run: python3 kempe_even_program_harness.py --min-n 6 --max-n 10
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict, deque
|
||||
from itertools import combinations
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PLANTRI = os.path.join(HERE, "..", "..", "..", "plantri", "plantri")
|
||||
|
||||
Edge = frozenset
|
||||
|
||||
|
||||
def e(a, b):
|
||||
return Edge((a, b))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plane triangulation with rotation system.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Tri:
|
||||
def __init__(self, rot: dict[int, list[int]]):
|
||||
self.rot = rot # vertex -> neighbours in cyclic (embedding) order
|
||||
|
||||
def copy(self):
|
||||
return Tri({v: list(ns) for v, ns in self.rot.items()})
|
||||
|
||||
def vertices(self):
|
||||
return list(self.rot)
|
||||
|
||||
def edges(self):
|
||||
return {e(u, v) for u in self.rot for v in self.rot[u]}
|
||||
|
||||
def faces(self):
|
||||
"""All faces as vertex triples (cyclic), via next-in-rotation walk."""
|
||||
seen = set()
|
||||
out = []
|
||||
for u in self.rot:
|
||||
for v in self.rot[u]:
|
||||
if (u, v) in seen:
|
||||
continue
|
||||
face = []
|
||||
cu, cv = u, v
|
||||
while (cu, cv) not in seen:
|
||||
seen.add((cu, cv))
|
||||
face.append(cu)
|
||||
ns = self.rot[cv]
|
||||
cw = ns[(ns.index(cu) + 1) % len(ns)]
|
||||
cu, cv = cv, cw
|
||||
out.append(tuple(face))
|
||||
return out
|
||||
|
||||
def check(self):
|
||||
nv = len(self.rot)
|
||||
ne = len(self.edges())
|
||||
fs = self.faces()
|
||||
assert ne == 3 * nv - 6, f"edge count {ne} != {3*nv-6}"
|
||||
assert all(len(f) == 3 for f in fs), "non-triangular face"
|
||||
assert len(fs) == 2 * nv - 4, f"face count {len(fs)}"
|
||||
|
||||
def apexes_of(self, u, v):
|
||||
"""The two third-vertices of the faces on edge uv."""
|
||||
out = []
|
||||
for f in (self._face_from(u, v), self._face_from(v, u)):
|
||||
out.append(next(w for w in f if w not in (u, v)))
|
||||
return out # [apex left of u->v, apex left of v->u]
|
||||
|
||||
def _face_from(self, u, v):
|
||||
face = []
|
||||
cu, cv = u, v
|
||||
for _ in range(3):
|
||||
face.append(cu)
|
||||
ns = self.rot[cv]
|
||||
cw = ns[(ns.index(cu) + 1) % len(ns)]
|
||||
cu, cv = cv, cw
|
||||
return face
|
||||
|
||||
# -- surgeries ----------------------------------------------------------
|
||||
|
||||
def _new_vertex(self):
|
||||
return max(self.rot) + 1
|
||||
|
||||
def insert_diamond(self, u, v):
|
||||
"""Delete uv; add w adjacent to u, v and both apexes. Returns
|
||||
(w, u, v, x, t) with x, t the two apexes."""
|
||||
x, t = self.apexes_of(u, v)
|
||||
w = self._new_vertex()
|
||||
for ordering in ([u, x, v, t], [u, t, v, x]):
|
||||
trial = self.copy()
|
||||
trial.rot[u][trial.rot[u].index(v)] = w
|
||||
trial.rot[v][trial.rot[v].index(u)] = w
|
||||
for a in (x, t):
|
||||
i, j = trial.rot[a].index(u), trial.rot[a].index(v)
|
||||
# insert w between u and v (they are cyclically adjacent at a)
|
||||
k = i if (i + 1) % len(trial.rot[a]) == j else j
|
||||
trial.rot[a].insert(k + 1, w)
|
||||
trial.rot[w] = ordering
|
||||
try:
|
||||
trial.check()
|
||||
except AssertionError:
|
||||
continue
|
||||
self.rot = trial.rot
|
||||
return (w, u, v, x, t)
|
||||
raise RuntimeError("diamond insertion failed both orientations")
|
||||
|
||||
def insert_leaf_gadget(self, u, v, t):
|
||||
"""Terminal face (u,v,t): add y on uv (under outer apex x) and hub z;
|
||||
delete uv; add xy,uy,vy,zy,zu,zv,zt. Returns (y, z, u, v, x, t)."""
|
||||
aps = self.apexes_of(u, v)
|
||||
x = next(a for a in aps if a != t)
|
||||
y, z = self._new_vertex(), self._new_vertex() + 1
|
||||
base = self.copy()
|
||||
for uord in ([y, z], [z, y]):
|
||||
for yord in ([x, v, z, u], [x, u, z, v]):
|
||||
for zord in ([y, v, t, u], [y, u, t, v]):
|
||||
trial = base.copy()
|
||||
iu = trial.rot[u].index(v)
|
||||
trial.rot[u][iu:iu + 1] = uord
|
||||
iv = trial.rot[v].index(u)
|
||||
trial.rot[v][iv:iv + 1] = list(reversed(uord))
|
||||
ix, jx = trial.rot[x].index(u), trial.rot[x].index(v)
|
||||
k = ix if (ix + 1) % len(trial.rot[x]) == jx else jx
|
||||
trial.rot[x].insert(k + 1, y)
|
||||
it_, jt = trial.rot[t].index(u), trial.rot[t].index(v)
|
||||
k = it_ if (it_ + 1) % len(trial.rot[t]) == jt else jt
|
||||
trial.rot[t].insert(k + 1, z)
|
||||
trial.rot[y] = yord
|
||||
trial.rot[z] = zord
|
||||
try:
|
||||
trial.check()
|
||||
# the gadget must create the four wheel faces
|
||||
fs = {frozenset(f) for f in trial.faces()}
|
||||
need = [{u, y, z}, {y, v, z}, {v, t, z}, {t, u, z},
|
||||
{x, u, y}, {x, y, v}]
|
||||
if not all(frozenset(s) in fs for s in need):
|
||||
continue
|
||||
except AssertionError:
|
||||
continue
|
||||
self.rot = trial.rot
|
||||
return (y, z, u, v, x, t)
|
||||
raise RuntimeError("leaf gadget insertion failed")
|
||||
|
||||
def remove_degree4(self, w, a, c):
|
||||
"""Remove degree-4 w, restoring diagonal ac (a, c opposite in rot[w])."""
|
||||
ns = self.rot[w]
|
||||
assert len(ns) == 4 and a in ns and c in ns
|
||||
assert (ns.index(a) - ns.index(c)) % 4 == 2, "diagonal not opposite"
|
||||
for p in ns:
|
||||
i = self.rot[p].index(w)
|
||||
if p == a:
|
||||
self.rot[p][i] = c
|
||||
elif p == c:
|
||||
self.rot[p][i] = a
|
||||
else:
|
||||
del self.rot[p][i]
|
||||
del self.rot[w]
|
||||
|
||||
def remove_degree3(self, w):
|
||||
for p in self.rot[w]:
|
||||
self.rot[p].remove(w)
|
||||
del self.rot[w]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# plantri reader.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def plantri_triangulations(n, limit=None):
|
||||
raw = subprocess.run([PLANTRI, str(n)], capture_output=True).stdout
|
||||
assert raw.startswith(b">>planar_code<<")
|
||||
data = raw[len(b">>planar_code<<"):]
|
||||
i = 0
|
||||
out = []
|
||||
while i < len(data) and (limit is None or len(out) < limit):
|
||||
nv = data[i]
|
||||
i += 1
|
||||
rot = {}
|
||||
for v in range(1, nv + 1):
|
||||
ns = []
|
||||
while data[i] != 0:
|
||||
ns.append(data[i])
|
||||
i += 1
|
||||
i += 1
|
||||
rot[v] = ns
|
||||
out.append(Tri(rot))
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Level / seam analysis.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Analysis:
|
||||
def __init__(self, g: Tri, outer: tuple[int, int, int]):
|
||||
self.g = g
|
||||
self.outer = outer
|
||||
self.level = self._levels()
|
||||
self.faces = g.faces()
|
||||
self.face_min = {frozenset(f): min(self.level[v] for v in f)
|
||||
for f in self.faces}
|
||||
self.degenerate = None
|
||||
self._classify()
|
||||
|
||||
def _levels(self):
|
||||
lev = {v: None for v in self.g.rot}
|
||||
q = deque()
|
||||
for v in self.outer:
|
||||
lev[v] = 0
|
||||
q.append(v)
|
||||
while q:
|
||||
u = q.popleft()
|
||||
for w in self.g.rot[u]:
|
||||
if lev[w] is None:
|
||||
lev[w] = lev[u] + 1
|
||||
q.append(w)
|
||||
return lev
|
||||
|
||||
def _classify(self):
|
||||
g, lev = self.g, self.level
|
||||
outer_face = frozenset(self.outer)
|
||||
self.flat_faces = [] # all-equal-level faces (excluding outer)
|
||||
for f in self.faces:
|
||||
ls = [lev[v] for v in f]
|
||||
if ls[0] == ls[1] == ls[2] and frozenset(f) != outer_face:
|
||||
self.flat_faces.append(tuple(f))
|
||||
# seam edges per depth k>=1: level-k edges with exactly one face of
|
||||
# min-level k-1
|
||||
self.seam_edges = defaultdict(set) # k -> set of edges
|
||||
self.fins = set()
|
||||
for edge in g.edges():
|
||||
a, b = tuple(edge)
|
||||
if lev[a] != lev[b]:
|
||||
continue
|
||||
k = lev[a]
|
||||
if k == 0:
|
||||
continue # outer triangle
|
||||
x, t = g.apexes_of(a, b)
|
||||
mins = sorted([min(lev[x], k), min(lev[t], k)])
|
||||
above = sum(1 for ap in (x, t) if lev[ap] == k - 1)
|
||||
if above == 1:
|
||||
self.seam_edges[k].add(edge)
|
||||
elif above == 2:
|
||||
self.fins.add(edge)
|
||||
else:
|
||||
self.degenerate = "chord-level-edge"
|
||||
return
|
||||
# seams must decompose into disjoint simple cycles
|
||||
self.seams = [] # list of (k, [edges], [vertices in cyclic order])
|
||||
for k, es in self.seam_edges.items():
|
||||
deg = defaultdict(list)
|
||||
for edge in es:
|
||||
a, b = tuple(edge)
|
||||
deg[a].append(b)
|
||||
deg[b].append(a)
|
||||
if any(len(ns) != 2 for ns in deg.values()):
|
||||
self.degenerate = "non-simple-seam"
|
||||
return
|
||||
left = set(es)
|
||||
while left:
|
||||
a0, b0 = tuple(next(iter(left)))
|
||||
cyc = [a0, b0]
|
||||
left.discard(e(a0, b0))
|
||||
while True:
|
||||
nxt = next(c for c in deg[cyc[-1]] if c != cyc[-2])
|
||||
if nxt == cyc[0]:
|
||||
break
|
||||
cyc.append(nxt)
|
||||
left.discard(e(cyc[-2], cyc[-1]))
|
||||
left.discard(e(cyc[-1], cyc[0]))
|
||||
self.seams.append((k, cyc))
|
||||
# terminal triangles: flat faces whose 3 edges are all seam edges
|
||||
self.terminal = []
|
||||
for f in self.flat_faces:
|
||||
a, b, c = f
|
||||
k = self.level[a]
|
||||
es = [e(a, b), e(b, c), e(c, a)]
|
||||
if all(x in self.seam_edges.get(k, ()) for x in es):
|
||||
self.terminal.append(f)
|
||||
else:
|
||||
self.degenerate = "non-terminal-flat-face"
|
||||
return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Medial graph + canonical colouring.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def medial_adj(g: Tri):
|
||||
adj = defaultdict(set)
|
||||
for v, ns in g.rot.items():
|
||||
d = len(ns)
|
||||
for i in range(d):
|
||||
a, b = e(v, ns[i]), e(v, ns[(i + 1) % d])
|
||||
adj[a].add(b)
|
||||
adj[b].add(a)
|
||||
return adj
|
||||
|
||||
|
||||
def annular_cycles(g: Tri, level):
|
||||
"""Spoke medial vertices grouped into annular cycles (per tread)."""
|
||||
spokes = [ed for ed in g.edges() if abs(level[min(ed)] - level[max(ed)]) == 1
|
||||
or True]
|
||||
spokes = [ed for ed in g.edges()
|
||||
if level[tuple(ed)[0]] != level[tuple(ed)[1]]]
|
||||
adj = medial_adj(g)
|
||||
sset = set(spokes)
|
||||
comp = []
|
||||
seen = set()
|
||||
for s in spokes:
|
||||
if s in seen:
|
||||
continue
|
||||
stack, cur = [s], set()
|
||||
seen.add(s)
|
||||
while stack:
|
||||
a = stack.pop()
|
||||
cur.add(a)
|
||||
for b in adj[a]:
|
||||
if b in sset and b not in seen:
|
||||
seen.add(b)
|
||||
stack.append(b)
|
||||
comp.append(cur)
|
||||
return comp, adj
|
||||
|
||||
|
||||
def order_cycle(verts, adj):
|
||||
"""Order a vertex set known to induce a cycle."""
|
||||
v0 = next(iter(verts))
|
||||
cyc = [v0]
|
||||
prev = None
|
||||
while True:
|
||||
nbrs = [w for w in adj[cyc[-1]] if w in verts and w != prev]
|
||||
if not nbrs:
|
||||
return None
|
||||
prev = cyc[-1]
|
||||
nxt = nbrs[0]
|
||||
if nxt == cyc[0]:
|
||||
return cyc if len(cyc) == len(verts) else None
|
||||
cyc.append(nxt)
|
||||
if len(cyc) > len(verts):
|
||||
return None
|
||||
|
||||
|
||||
def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
"""Canonical colouring of M(g): level-edge medials -> 2 ("colour 3"),
|
||||
non-root annuli alternate 0,1 (random phases), root region solved by DFS.
|
||||
Returns (coloring dict, adj) or (None, reason)."""
|
||||
adj = medial_adj(g)
|
||||
col = {}
|
||||
level_edges = [ed for ed in g.edges()
|
||||
if level[tuple(ed)[0]] == level[tuple(ed)[1]]]
|
||||
outer_es = {e(outer[0], outer[1]), e(outer[1], outer[2]),
|
||||
e(outer[2], outer[0])}
|
||||
for ed in level_edges:
|
||||
if ed not in outer_es:
|
||||
col[ed] = 2
|
||||
comps, _ = annular_cycles(g, level)
|
||||
root_comp = None
|
||||
free = set(outer_es)
|
||||
for comp in comps:
|
||||
cyc = order_cycle(comp, adj)
|
||||
if cyc is None:
|
||||
return None, "annulus-not-cycle"
|
||||
depth = min(level[v] for ed in comp for v in ed)
|
||||
if depth == 0: # root annulus (spokes touching level 0)
|
||||
if root_comp is not None:
|
||||
return None, "multiple-root-annuli"
|
||||
root_comp = cyc
|
||||
free |= set(cyc)
|
||||
continue
|
||||
if len(cyc) % 2 != 0:
|
||||
return None, f"odd-non-root-annulus(len {len(cyc)})"
|
||||
phase = rng.randint(0, 1) if rng else 0
|
||||
for i, m in enumerate(cyc):
|
||||
col[m] = (i + phase) % 2
|
||||
if root_comp is None:
|
||||
return None, "no-root-annulus"
|
||||
# solve the free region (root annulus + outer trio) by DFS; if that
|
||||
# fails, grow the defect region one level at a time.
|
||||
max_level = max(level.values())
|
||||
for grow in range(0, max_level + 1):
|
||||
free_now = set(free)
|
||||
if grow:
|
||||
for ed in g.edges():
|
||||
a, b = tuple(ed)
|
||||
if min(level[a], level[b]) < grow:
|
||||
free_now.add(ed)
|
||||
trial = {m: c for m, c in col.items() if m not in free_now}
|
||||
order = sorted(free_now,
|
||||
key=lambda m: -sum(1 for x in adj[m] if x in trial))
|
||||
colorder = [0, 1, 2]
|
||||
if rng:
|
||||
rng.shuffle(colorder)
|
||||
|
||||
def ok(m, c):
|
||||
return all(trial.get(x) != c for x in adj[m])
|
||||
|
||||
def dfs(i):
|
||||
if i == len(order):
|
||||
return True
|
||||
m = order[i]
|
||||
for c in colorder:
|
||||
if ok(m, c):
|
||||
trial[m] = c
|
||||
if dfs(i + 1):
|
||||
return True
|
||||
del trial[m]
|
||||
return False
|
||||
|
||||
if dfs(0):
|
||||
for m, c in trial.items():
|
||||
for x in adj[m]:
|
||||
assert trial.get(x) != c, "canonical colouring improper"
|
||||
return trial, None
|
||||
if len(free_now) > 60: # defect region growing unmanageable
|
||||
break
|
||||
return None, "root-unsolvable"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kempe switching + removal conditions.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def kempe_component(col, adj, start, pair):
|
||||
if col[start] not in pair:
|
||||
return None
|
||||
comp = {start}
|
||||
stack = [start]
|
||||
while stack:
|
||||
a = stack.pop()
|
||||
for b in adj[a]:
|
||||
if b in col and col[b] in pair and b not in comp:
|
||||
comp.add(b)
|
||||
stack.append(b)
|
||||
return frozenset(comp)
|
||||
|
||||
|
||||
def switch(col, comp, pair):
|
||||
a, b = pair
|
||||
for m in comp:
|
||||
col[m] = b if col[m] == a else a
|
||||
|
||||
|
||||
def diamond_condition(col, w_quad):
|
||||
"""w_quad = (a, b, c, d) cyclic rot of planted vertex, diagonal (a, c).
|
||||
Quad edges ab, bc, cd, da; new faces a-b-c and a-c-d."""
|
||||
a, b, c, d = w_quad
|
||||
p, q, r, s = col[e(a, b)], col[e(b, c)], col[e(c, d)], col[e(d, a)]
|
||||
if p == q or r == s:
|
||||
return None
|
||||
used = {p, q, r, s}
|
||||
if len(used) > 2:
|
||||
return None
|
||||
return ({0, 1, 2} - used).pop()
|
||||
|
||||
|
||||
def rainbow_condition(col, tri):
|
||||
a, b, c = tri
|
||||
cols = {col[e(a, b)], col[e(b, c)], col[e(c, a)]}
|
||||
return len(cols) == 3
|
||||
|
||||
|
||||
def try_establish(col, adj, support, test, max_switch=3):
|
||||
"""Bounded search: switch <= max_switch Kempe components (any pair)
|
||||
anchored at the support medials (and their coloured neighbours) to make
|
||||
test(col) true. Mutates col on success; restores on failure."""
|
||||
if test(col):
|
||||
return True
|
||||
anchors = set(support)
|
||||
for m in support:
|
||||
anchors.update(adj[m])
|
||||
cands = []
|
||||
seenc = set()
|
||||
for m in anchors:
|
||||
if m not in col:
|
||||
continue
|
||||
for pair in ((0, 1), (0, 2), (1, 2)):
|
||||
comp = kempe_component(col, adj, m, pair)
|
||||
if comp and (comp, pair) not in seenc:
|
||||
seenc.add((comp, pair))
|
||||
cands.append((comp, pair))
|
||||
for k in range(1, max_switch + 1):
|
||||
for subset in combinations(range(len(cands)), k):
|
||||
# only co-switch components that are pairwise vertex-disjoint
|
||||
verts = set()
|
||||
clash = False
|
||||
for idx in subset:
|
||||
if verts & cands[idx][0]:
|
||||
clash = True
|
||||
break
|
||||
verts |= cands[idx][0]
|
||||
if clash:
|
||||
continue
|
||||
for idx in subset:
|
||||
switch(col, *cands[idx])
|
||||
if test(col):
|
||||
return True
|
||||
for idx in subset:
|
||||
switch(col, *cands[idx])
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The per-graph pipeline.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def quad_of(g, w, diag_a, diag_c):
|
||||
ns = g.rot[w]
|
||||
i = ns.index(diag_a)
|
||||
if ns[(i + 2) % 4] != diag_c:
|
||||
return None
|
||||
return (ns[i], ns[(i + 1) % 4], ns[(i + 2) % 4], ns[(i + 3) % 4])
|
||||
|
||||
|
||||
def collapse_degree4(g, col, w, a, c):
|
||||
"""Remove planted degree-4 w restoring diagonal ac; col gets col[ac]."""
|
||||
quad = quad_of(g, w, a, c)
|
||||
third = diamond_condition(col, quad)
|
||||
assert third is not None
|
||||
for p in g.rot[w]:
|
||||
col.pop(e(w, p), None)
|
||||
g.remove_degree4(w, a, c)
|
||||
col[e(a, c)] = third
|
||||
|
||||
|
||||
def collapse_degree3(g, col, w):
|
||||
for p in g.rot[w]:
|
||||
col.pop(e(w, p), None)
|
||||
g.remove_degree3(w)
|
||||
|
||||
|
||||
def verify_proper(g, col):
|
||||
adj = medial_adj(g)
|
||||
for m in adj:
|
||||
assert m in col, f"uncoloured medial {m}"
|
||||
for b in adj[m]:
|
||||
if col[m] == col[b]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_graph(g0: Tri, outer=None, verbose=False, attempts=4):
|
||||
import random
|
||||
g0.check()
|
||||
if outer is None:
|
||||
outer = tuple(g0.faces()[0])
|
||||
an0 = Analysis(g0.copy(), outer)
|
||||
if an0.degenerate:
|
||||
return f"skip:{an0.degenerate}"
|
||||
last = "fail:unknown"
|
||||
for att in range(attempts):
|
||||
rng = random.Random(1000 + att)
|
||||
last = _attempt(g0, outer, rng, verbose)
|
||||
if last == "ok" or last.startswith("skip"):
|
||||
return last
|
||||
return last
|
||||
|
||||
|
||||
def _attempt(g0: Tri, outer, rng, verbose=False):
|
||||
g = g0.copy()
|
||||
an = Analysis(g, outer)
|
||||
if an.degenerate:
|
||||
return f"skip:{an.degenerate}"
|
||||
|
||||
# --- surgeries -------------------------------------------------------
|
||||
diamonds = [] # (w, quad(a,b,c,d) with diagonal (u,v)) for removal
|
||||
gadgets = [] # (y, z, u, v, x, t)
|
||||
for f in an.terminal:
|
||||
a, b, c = f
|
||||
y, z, u, v, x, t = g.insert_leaf_gadget(a, b, c)
|
||||
gadgets.append((y, z, u, v, x, t))
|
||||
# re-analyse (gadgets change seams)
|
||||
an = Analysis(g, outer)
|
||||
if an.degenerate:
|
||||
return f"skip:post-gadget-{an.degenerate}"
|
||||
for k, cyc in an.seams:
|
||||
if len(cyc) % 2 == 0:
|
||||
continue
|
||||
# choose a seam edge whose below-apex is strictly deeper
|
||||
choice = None
|
||||
for i in range(len(cyc)):
|
||||
a, b = cyc[i], cyc[(i + 1) % len(cyc)]
|
||||
x, t = g.apexes_of(a, b)
|
||||
lx, lt = an.level[x], an.level[t]
|
||||
if {lx, lt} == {k - 1, k + 1}:
|
||||
choice = (a, b)
|
||||
break
|
||||
if choice is None:
|
||||
return "skip:no-diamond-site"
|
||||
w, u, v, x, t = g.insert_diamond(*choice)
|
||||
diamonds.append((w, u, v, x, t))
|
||||
an = Analysis(g, outer)
|
||||
if an.degenerate:
|
||||
return f"skip:post-diamond-{an.degenerate}"
|
||||
if any(len(cyc) % 2 for _, cyc in an.seams):
|
||||
return "fail:seam-evening"
|
||||
|
||||
# --- canonical colouring ----------------------------------------------
|
||||
col, reason = canonical_coloring(g, an.level, outer, rng=rng)
|
||||
if col is None:
|
||||
return f"fail:canonical-{reason}"
|
||||
|
||||
# --- descent -----------------------------------------------------------
|
||||
level = an.level
|
||||
# diamonds: remove each (diagonal uv)
|
||||
for w, u, v, x, t in diamonds:
|
||||
adj = medial_adj(g)
|
||||
quad = quad_of(g, w, u, v)
|
||||
if quad is None:
|
||||
return "fail:diamond-quad"
|
||||
support = [e(quad[i], quad[(i + 1) % 4]) for i in range(4)]
|
||||
if not try_establish(col, adj, support,
|
||||
lambda c: diamond_condition(c, quad) is not None):
|
||||
return "fail:diamond-switch"
|
||||
collapse_degree4(g, col, w, u, v)
|
||||
if not verify_proper(g, col):
|
||||
return "fail:improper-after-diamond"
|
||||
# gadgets: try order A (z then y) else order B (y then z)
|
||||
for y, z, u, v, x, t in gadgets:
|
||||
done = False
|
||||
for first, second, tri in ((z, y, (x, u, v)), (y, z, (u, v, t))):
|
||||
gtrial, ctrial = g.copy(), dict(col)
|
||||
adj = medial_adj(gtrial)
|
||||
quad = quad_of(gtrial, first, u, v)
|
||||
if quad is None:
|
||||
continue
|
||||
support = [e(quad[i], quad[(i + 1) % 4]) for i in range(4)]
|
||||
if not try_establish(ctrial, adj, support,
|
||||
lambda c: diamond_condition(c, quad) is not None):
|
||||
continue
|
||||
collapse_degree4(gtrial, ctrial, first, u, v)
|
||||
if not verify_proper(gtrial, ctrial):
|
||||
continue
|
||||
adj2 = medial_adj(gtrial)
|
||||
a_, b_, c_ = tri
|
||||
support2 = [e(a_, b_), e(b_, c_), e(c_, a_)]
|
||||
if not try_establish(ctrial, adj2, support2,
|
||||
lambda c: rainbow_condition(c, tri)):
|
||||
continue
|
||||
collapse_degree3(gtrial, ctrial, second)
|
||||
if not verify_proper(gtrial, ctrial):
|
||||
continue
|
||||
g, col = gtrial, ctrial
|
||||
done = True
|
||||
break
|
||||
if not done:
|
||||
return "fail:gadget-removal"
|
||||
|
||||
# --- final check --------------------------------------------------------
|
||||
if set(g.rot) != set(g0.rot):
|
||||
return "fail:vertex-mismatch"
|
||||
if not verify_proper(g, col):
|
||||
return "fail:final-improper"
|
||||
return "ok"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Driver.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Synthetic clean ring triangulations (the programme's natural domain).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def tri_from_faces(faces):
|
||||
"""Build a Tri from a consistently oriented face list covering the sphere
|
||||
(every directed edge used exactly once)."""
|
||||
succ = defaultdict(dict)
|
||||
for a, b, c in faces:
|
||||
succ[a][b] = c
|
||||
succ[b][c] = a
|
||||
succ[c][a] = b
|
||||
rot = {}
|
||||
for v, nx in succ.items():
|
||||
start = next(iter(nx))
|
||||
cyc = [start]
|
||||
for _ in range(len(nx)):
|
||||
w = nx.get(cyc[-1])
|
||||
if w is None or w == start:
|
||||
break
|
||||
cyc.append(w)
|
||||
if len(cyc) != len(nx) or nx.get(cyc[-1]) != start:
|
||||
return None
|
||||
rot[v] = cyc
|
||||
return Tri(rot)
|
||||
|
||||
|
||||
def ring_triangulation(ring_sizes, leaf, rng):
|
||||
"""Concentric ring triangulation: ring_sizes[0] must be 3 (outer
|
||||
triangle); annuli between consecutive rings get random tooth words;
|
||||
leaf in {'hub','face'} ('face' needs innermost size 3)."""
|
||||
assert ring_sizes[0] == 3
|
||||
rings = []
|
||||
nid = 0
|
||||
for r in ring_sizes:
|
||||
rings.append(list(range(nid, nid + r)))
|
||||
nid += r
|
||||
faces = []
|
||||
for k in range(len(rings) - 1):
|
||||
A, B = rings[k], rings[k + 1]
|
||||
a, b = len(A), len(B)
|
||||
for _ in range(50): # reject words that revisit a spoke
|
||||
word = ["A"] * a + ["B"] * b
|
||||
rng.shuffle(word)
|
||||
spokes = {(0, 0)}
|
||||
i = j = 0
|
||||
good = True
|
||||
for mv in word[:-1]:
|
||||
if mv == "A":
|
||||
i += 1
|
||||
else:
|
||||
j += 1
|
||||
if (i % a, j % b) in spokes:
|
||||
good = False
|
||||
break
|
||||
spokes.add((i % a, j % b))
|
||||
if good:
|
||||
break
|
||||
else:
|
||||
return None, None
|
||||
i = j = 0
|
||||
for mv in word:
|
||||
if mv == "A":
|
||||
faces.append((A[i % a], A[(i + 1) % a], B[j % b]))
|
||||
i += 1
|
||||
else:
|
||||
faces.append((B[(j + 1) % b], B[j % b], A[i % a]))
|
||||
j += 1
|
||||
inner = rings[-1]
|
||||
if leaf == "hub":
|
||||
h = nid
|
||||
for j in range(len(inner)):
|
||||
faces.append((inner[j], inner[(j + 1) % len(inner)], h))
|
||||
else:
|
||||
assert len(inner) == 3
|
||||
faces.append((inner[0], inner[1], inner[2]))
|
||||
outer = rings[0]
|
||||
faces.append((outer[2], outer[1], outer[0])) # the unbounded face
|
||||
for variant in (faces, [tuple(reversed(f)) for f in faces]):
|
||||
g = tri_from_faces(variant)
|
||||
if g is None:
|
||||
continue
|
||||
try:
|
||||
g.check()
|
||||
return g, tuple(outer)
|
||||
except AssertionError:
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def random_profile(rng):
|
||||
depth = rng.randint(2, 4)
|
||||
sizes = [3] + [rng.randint(3, 8) for _ in range(depth)]
|
||||
leaf = "face" if (sizes[-1] == 3 and rng.random() < 0.5) else "hub"
|
||||
if leaf == "face" and sizes[-1] != 3:
|
||||
leaf = "hub"
|
||||
return sizes, leaf
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--min-n", type=int, default=6)
|
||||
ap.add_argument("--max-n", type=int, default=10)
|
||||
ap.add_argument("--limit", type=int, default=None)
|
||||
ap.add_argument("--synthetic", type=int, default=0,
|
||||
help="number of random ring triangulations to test")
|
||||
ap.add_argument("--seed", type=int, default=1)
|
||||
ap.add_argument("--verbose", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
import random
|
||||
grand = defaultdict(int)
|
||||
if args.synthetic:
|
||||
rng = random.Random(args.seed)
|
||||
tally = defaultdict(int)
|
||||
for idx in range(args.synthetic):
|
||||
sizes, leaf = random_profile(rng)
|
||||
g, outer = ring_triangulation(sizes, leaf, rng)
|
||||
if g is None:
|
||||
tally["error:construction"] += 1
|
||||
continue
|
||||
try:
|
||||
res = run_graph(g, outer=outer, verbose=args.verbose)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
res = f"error:{type(ex).__name__}"
|
||||
tally[res] += 1
|
||||
grand[res] += 1
|
||||
if args.verbose and not res.startswith("ok"):
|
||||
print(f" synth #{idx} sizes={sizes} leaf={leaf}: {res}")
|
||||
line = " ".join(f"{k}={v}" for k, v in sorted(tally.items()))
|
||||
print(f"synthetic ({args.synthetic}): {line}")
|
||||
else:
|
||||
for n in range(args.min_n, args.max_n + 1):
|
||||
tally = defaultdict(int)
|
||||
gs = plantri_triangulations(n, args.limit)
|
||||
for idx, g in enumerate(gs):
|
||||
try:
|
||||
res = run_graph(g, verbose=args.verbose)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
res = f"error:{type(ex).__name__}"
|
||||
tally[res] += 1
|
||||
grand[res] += 1
|
||||
if args.verbose and not res.startswith(("ok", "skip")):
|
||||
print(f" n={n} #{idx}: {res}")
|
||||
line = " ".join(f"{k}={v}" for k, v in sorted(tally.items()))
|
||||
print(f"n={n} ({len(gs)} graphs): {line}")
|
||||
sys.stdout.flush()
|
||||
print("\nTOTAL: " + " ".join(f"{k}={v}" for k, v in sorted(grand.items())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user