Treat each disjoint annular cycle as its own full medial tire graph
A tread's annular frontier can split into several disjoint cycles; each is now recognised as a separate full medial tire graph instead of disqualifying the whole tread. - recognise() returns a list of (g, bij), one per annular cycle component; add annular_cycle_components() and _recognise_one(), and iterate components in iter_pieces(). - Key tires/results by (depth, component) throughout both experiment drivers: _label_treads chains each tire to a parent-depth down tooth sharing its apex; _cap_cut/_assemble_cut_graph/to_json/summary and the dual-cut collectors/draws follow suit. Source vertex selection for the dual-cut experiment now deep-embeds a random face and roots at the outer-cap vertex. The source-dual figure labels the source-graph vertices, highlights the entry medial vertex, and uses a cap-rooted concentric layout. For seed 7 / face (14,15,19) this recognises treads 3 and 4 as two tires each (3.0,3.1,4.0,4.1), so every dual face is now cut. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+95
-56
@@ -178,19 +178,17 @@ def extract_tread(faces, levels, d):
|
||||
}
|
||||
|
||||
|
||||
def annular_cycle_order(M: nx.Graph, annular: set):
|
||||
"""Cyclic order of the annular medial vertices (they induce a cycle)."""
|
||||
sub = M.subgraph(annular)
|
||||
if sub.number_of_nodes() == 0 or any(sub.degree(v) != 2 for v in sub):
|
||||
def _cycle_order(sub: nx.Graph, comp):
|
||||
"""Cyclic order of a 2-regular connected component ``comp`` of ``sub``;
|
||||
None if it is not a single simple cycle of length >= 3."""
|
||||
csub = sub.subgraph(comp)
|
||||
if csub.number_of_nodes() < 3 or any(csub.degree(v) != 2 for v in csub):
|
||||
return None
|
||||
if not nx.is_connected(sub):
|
||||
return None
|
||||
start = next(iter(annular))
|
||||
start = next(iter(comp))
|
||||
order = [start]
|
||||
prev = None
|
||||
cur = start
|
||||
prev, cur = None, start
|
||||
while True:
|
||||
nbrs = [w for w in sub.neighbors(cur) if w != prev]
|
||||
nbrs = [w for w in csub.neighbors(cur) if w != prev]
|
||||
if not nbrs:
|
||||
break
|
||||
nxt = nbrs[0]
|
||||
@@ -198,7 +196,33 @@ def annular_cycle_order(M: nx.Graph, annular: set):
|
||||
break
|
||||
order.append(nxt)
|
||||
prev, cur = cur, nxt
|
||||
return order if len(order) == len(annular) else None
|
||||
return order if len(order) == csub.number_of_nodes() else None
|
||||
|
||||
|
||||
def annular_cycle_order(M: nx.Graph, annular: set):
|
||||
"""Cyclic order of the annular medial vertices when they induce a *single*
|
||||
cycle; None otherwise. See ``annular_cycle_components`` for the
|
||||
multi-component case."""
|
||||
sub = M.subgraph(annular)
|
||||
if not annular or not nx.is_connected(sub):
|
||||
return None
|
||||
return _cycle_order(sub, set(annular))
|
||||
|
||||
|
||||
def annular_cycle_components(M: nx.Graph, annular: set):
|
||||
"""Cyclic orders of the annular medial vertices, one per connected
|
||||
component of the annular subgraph.
|
||||
|
||||
A tread's annular frontier may split into several disjoint cycles (one per
|
||||
boundary component); each is its own full medial tire graph. Components
|
||||
that are not a single simple cycle of length >= 3 are skipped."""
|
||||
sub = M.subgraph(annular)
|
||||
orders = []
|
||||
for comp in nx.connected_components(sub):
|
||||
order = _cycle_order(sub, comp)
|
||||
if order is not None:
|
||||
orders.append(order)
|
||||
return orders
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -226,38 +250,35 @@ def _linear_cut(n, bite_pairs):
|
||||
return None
|
||||
|
||||
|
||||
def recognise(M, tread):
|
||||
"""Return (FullMedialTireGraph, bijection fmt-name -> medial vertex) or None.
|
||||
def _recognise_one(M, order, up, ann_global):
|
||||
"""Recognise a single annular cycle (given as the cyclic order of its
|
||||
medial vertices) as a ``FullMedialTireGraph``.
|
||||
|
||||
``M`` here is the tread-face model M(T) (cycle + teeth + bites)."""
|
||||
annular = tread["annular"]
|
||||
order = annular_cycle_order(M, annular)
|
||||
if order is None or len(order) < 3:
|
||||
return None
|
||||
``up`` is the tread's up-edge medial-vertex set; ``ann_global`` is the full
|
||||
annular set of the tread (used to exclude annular vertices, including those
|
||||
of *other* components, when picking each cycle edge's apex). Returns
|
||||
``(g, bij)`` or None."""
|
||||
n = len(order)
|
||||
ann_set = set(annular)
|
||||
if n < 3:
|
||||
return None
|
||||
ann_set = set(order)
|
||||
|
||||
apex_of_edge = []
|
||||
for i in range(n):
|
||||
a, b = order[i], order[(i + 1) % n]
|
||||
common = [w for w in set(M.neighbors(a)) & set(M.neighbors(b)) if w not in ann_set]
|
||||
common = [w for w in set(M.neighbors(a)) & set(M.neighbors(b))
|
||||
if w not in ann_global]
|
||||
if len(common) != 1:
|
||||
return None
|
||||
apex_of_edge.append(common[0])
|
||||
|
||||
up = set(tread["up"])
|
||||
# bite apex: serves two cycle edges (== adjacent to four annular vertices)
|
||||
apex_positions = defaultdict(list)
|
||||
for i, ap in enumerate(apex_of_edge):
|
||||
apex_positions[ap].append(i)
|
||||
|
||||
tooth = []
|
||||
bite_pairs = []
|
||||
for ap, positions in apex_positions.items():
|
||||
if len(positions) == 2:
|
||||
bite_pairs.append(tuple(sorted(positions)))
|
||||
for i, ap in enumerate(apex_of_edge):
|
||||
tooth.append("U" if ap in up else "D")
|
||||
bite_pairs = [tuple(sorted(positions))
|
||||
for positions in apex_positions.values() if len(positions) == 2]
|
||||
tooth = ["U" if ap in up else "D" for ap in apex_of_edge]
|
||||
|
||||
cut = _linear_cut(n, bite_pairs)
|
||||
if cut is None:
|
||||
@@ -279,14 +300,35 @@ def recognise(M, tread):
|
||||
for (i, j) in sorted(g.bites):
|
||||
bij[f"p{i}_{j}"] = apex_of_edge[(i + r) % n]
|
||||
|
||||
# verify the reconstructed graph is edge-faithful to the tread-face M(T)
|
||||
mt_edges = {ekey(*e) for e in M.edges()}
|
||||
# verify the reconstructed graph is edge-faithful to this cycle's sub-model
|
||||
# (its annular vertices together with their tooth apexes).
|
||||
sub_nodes = ann_set | set(apex_of_edge)
|
||||
sub_edges = {ekey(*e) for e in M.subgraph(sub_nodes).edges()}
|
||||
rec_edges = {ekey(bij[u], bij[v]) for u, v in g.edges()}
|
||||
if rec_edges != mt_edges:
|
||||
if rec_edges != sub_edges:
|
||||
return None
|
||||
return g, bij
|
||||
|
||||
|
||||
def recognise(M, tread):
|
||||
"""Recognise the tread's medial-tire structure.
|
||||
|
||||
A tread's annular frontier may be several disjoint cycles, each its own
|
||||
full medial tire graph. Returns a list of ``(FullMedialTireGraph,
|
||||
bijection fmt-name -> medial vertex)`` -- one per annular cycle component
|
||||
that recognises -- or ``[]`` if none do.
|
||||
|
||||
``M`` here is the tread-face model M(T) (cycle(s) + teeth + bites)."""
|
||||
up = set(tread["up"])
|
||||
ann_global = set(tread["annular"])
|
||||
tires = []
|
||||
for order in annular_cycle_components(M, tread["annular"]):
|
||||
rec = _recognise_one(M, order, up, ann_global)
|
||||
if rec is not None:
|
||||
tires.append(rec)
|
||||
return tires
|
||||
|
||||
|
||||
def canonical(coloring, ordered):
|
||||
remap, out = {}, []
|
||||
for v in ordered:
|
||||
@@ -341,32 +383,29 @@ def iter_pieces(seed: int, color_limit: int = 400000):
|
||||
if tread is None or len(tread["up"]) < 3:
|
||||
continue
|
||||
mt = medial_tire_facemodel(tread["tread_faces"])
|
||||
rec = recognise(mt, tread)
|
||||
if rec is None:
|
||||
continue
|
||||
g, bij = rec
|
||||
mt_nodes = list(bij.values())
|
||||
name_of = {v: k for k, v in bij.items()}
|
||||
for comp, (g, bij) in enumerate(recognise(mt, tread)):
|
||||
mt_nodes = list(bij.values())
|
||||
name_of = {v: k for k, v in bij.items()}
|
||||
|
||||
realized = set()
|
||||
for col in global_colorings:
|
||||
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
|
||||
realized = set()
|
||||
for col in global_colorings:
|
||||
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
|
||||
|
||||
colorings = []
|
||||
seen = set()
|
||||
for col in proper_3_colorings_subgraph(mt, mt_nodes):
|
||||
key = canonical(col, mt_nodes)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
fmt_col = {name_of[v]: c for v, c in col.items()}
|
||||
balanced = kempe_classify(g, fmt_col).valid
|
||||
is_real = key in realized
|
||||
cat = ("Invalid" if not balanced
|
||||
else "Realized" if is_real else "Unrealized")
|
||||
colorings.append((fmt_col, cat))
|
||||
meta = {"source": s, "tread": d}
|
||||
yield (meta, g, colorings)
|
||||
colorings = []
|
||||
seen = set()
|
||||
for col in proper_3_colorings_subgraph(mt, mt_nodes):
|
||||
key = canonical(col, mt_nodes)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
fmt_col = {name_of[v]: c for v, c in col.items()}
|
||||
balanced = kempe_classify(g, fmt_col).valid
|
||||
is_real = key in realized
|
||||
cat = ("Invalid" if not balanced
|
||||
else "Realized" if is_real else "Unrealized")
|
||||
colorings.append((fmt_col, cat))
|
||||
meta = {"source": s, "tread": d, "comp": comp}
|
||||
yield (meta, g, colorings)
|
||||
|
||||
|
||||
def analyse(seed: int, color_limit: int = 400000):
|
||||
|
||||
Reference in New Issue
Block a user