Handle compound medial tires in cut labelling
This commit is contained in:
Binary file not shown.
@@ -57,12 +57,11 @@ sys.path.insert(0, _MTD)
|
||||
sys.path.insert(0, _CUT_LIB)
|
||||
|
||||
from tire_realization_analysis import ( # noqa: E402
|
||||
ekey, extract_tread, medial_graph, medial_tire_facemodel,
|
||||
recognise, triangular_faces,
|
||||
ekey, medial_graph, triangular_faces,
|
||||
)
|
||||
from run_medial_tire_cut_experiment import ( # noqa: E402
|
||||
_assemble_cut_graph, _cap_cut, _label_treads,
|
||||
random_maximal_planar_min_degree,
|
||||
_assemble_cut_graph, _build_treads, _cap_cut, _label_treads,
|
||||
random_maximal_planar_5_connected,
|
||||
)
|
||||
from medial_tire_cut_labelling import up_apex_cuts # noqa: E402
|
||||
|
||||
@@ -71,31 +70,6 @@ from medial_tire_cut_labelling import up_apex_cuts # noqa: E402
|
||||
# Tread recognition and the source-dual graph.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _build_treads(faces, levels):
|
||||
"""Recognise the full medial tire graph(s) of every BFS-level tread.
|
||||
|
||||
A tread depth whose annular frontier splits into several disjoint cycles
|
||||
yields one tire per cycle. Returns ``(treads, skipped)`` where ``treads``
|
||||
maps ``(depth, component)`` to the recognised ``(g, bij)`` and ``skipped``
|
||||
lists ``(d, reason)`` for the depths that produced no tire.
|
||||
"""
|
||||
treads, skipped = {}, []
|
||||
for d in range(max(levels.values())):
|
||||
tread = extract_tread(faces, levels, d)
|
||||
if tread is None:
|
||||
skipped.append((d, "no tread faces"))
|
||||
continue
|
||||
if len(tread["up"]) < 3:
|
||||
skipped.append((d, f"only {len(tread['up'])} up teeth"))
|
||||
continue
|
||||
tires = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
|
||||
if not tires:
|
||||
skipped.append((d, "no annular cycle recognised as a tire"))
|
||||
continue
|
||||
for c, gb in enumerate(tires):
|
||||
treads[(d, c)] = gb
|
||||
return treads, skipped
|
||||
|
||||
|
||||
def root_entry_choices(G, source):
|
||||
"""Edge indices of the root tread's up teeth -- the eligible entry teeth.
|
||||
@@ -104,7 +78,7 @@ def root_entry_choices(G, source):
|
||||
"""
|
||||
faces, _ = triangular_faces(G)
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
treads, _ = _build_treads(faces, levels)
|
||||
treads, _skipped, _meta = _build_treads(faces, levels)
|
||||
if not treads:
|
||||
return []
|
||||
g, _bij = treads[min(treads)]
|
||||
@@ -209,12 +183,28 @@ def annular_cut_edges(results, cap_cuts):
|
||||
def up_apex_cut_edges(results):
|
||||
"""Primal edges whose dual edge the apex duplications remove: the apex
|
||||
medial vertex of every up tooth across all treads, except each tread's
|
||||
entry tooth and any vertex that is the shared apex of two up teeth."""
|
||||
entry tooth and any vertex that is the shared apex of two up teeth within
|
||||
that recognised annular cycle. In a compound component, an entry apex is
|
||||
treated as an entry seam for every cycle sharing that apex.
|
||||
"""
|
||||
entry_apexes = defaultdict(set)
|
||||
for key in sorted(results):
|
||||
rec = results[key]
|
||||
compound = rec.get("compound", key)
|
||||
entry_apexes[compound].add(rec["bij"][f"u{rec['entry_edge']}"])
|
||||
|
||||
removed = set()
|
||||
for key in sorted(results):
|
||||
g, bij = results[key]["g"], results[key]["bij"]
|
||||
entry = results[key]["entry_edge"]
|
||||
removed.update(up_apex_cuts(g, entry, bij=bij).values())
|
||||
rec = results[key]
|
||||
g, bij = rec["g"], rec["bij"]
|
||||
entry = rec["entry_edge"]
|
||||
compound = rec.get("compound", key)
|
||||
removed.update(
|
||||
up_apex_cuts(
|
||||
g, entry, bij=bij,
|
||||
shared_apexes=entry_apexes.get(compound, set()),
|
||||
).values()
|
||||
)
|
||||
return removed
|
||||
|
||||
|
||||
@@ -245,7 +235,7 @@ def medial_tire_dual_cut(G, source, entry_edge):
|
||||
faces, emb = triangular_faces(G)
|
||||
M = medial_graph(G)
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
treads, skipped = _build_treads(faces, levels)
|
||||
treads, skipped, tread_meta = _build_treads(faces, levels)
|
||||
if not treads:
|
||||
raise ValueError(f"level source {source} induces no recognised tread")
|
||||
|
||||
@@ -256,7 +246,8 @@ def medial_tire_dual_cut(G, source, entry_edge):
|
||||
f"(choices: {sorted(g_root.up_edges)})")
|
||||
|
||||
results = {}
|
||||
_label_treads(treads, results, root_entry_edge=entry_edge)
|
||||
_label_treads(
|
||||
treads, results, root_entry_edge=entry_edge, tread_meta=tread_meta)
|
||||
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
||||
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
||||
|
||||
@@ -276,7 +267,8 @@ def medial_tire_dual_cut(G, source, entry_edge):
|
||||
"G": G, "M": M, "source": source, "entry_edge": entry_edge,
|
||||
"entry_medial_vertex": entry_medial,
|
||||
"faces": faces, "outer_face": 0,
|
||||
"levels": levels, "treads": treads, "skipped": skipped,
|
||||
"levels": levels, "treads": treads, "tread_meta": tread_meta,
|
||||
"skipped": skipped,
|
||||
"results": results, "cap_cuts": cap_cuts, "cut_graph": cut_graph,
|
||||
"labels": labels, "warnings": warnings,
|
||||
"dual": dual, "removed_dual_edges": removed,
|
||||
@@ -319,8 +311,11 @@ def dual_cut_random_face(G, rng=None):
|
||||
raise ValueError("no face's cap vertex induces a recognised root tread")
|
||||
|
||||
|
||||
def random_dual_cut(n=20, seed=0, rng=None, min_degree=5, flips=400, attempts=1000):
|
||||
"""Find a random maximal planar graph of minimum degree ``min_degree``, then
|
||||
def random_dual_cut(
|
||||
n=20, seed=0, rng=None, min_connectivity=5, flips=400, attempts=1000,
|
||||
min_degree=None,
|
||||
):
|
||||
"""Find a random maximal planar graph of node connectivity 5, then
|
||||
``dual_cut_random_face``.
|
||||
|
||||
``seed`` drives the graph sample; ``rng`` (defaulting to ``Random(seed)``)
|
||||
@@ -328,12 +323,16 @@ def random_dual_cut(n=20, seed=0, rng=None, min_degree=5, flips=400, attempts=10
|
||||
pipeline is reproducible from ``(n, seed)``.
|
||||
"""
|
||||
rng = rng or random.Random(seed)
|
||||
G, graph_seed = random_maximal_planar_min_degree(
|
||||
n, seed, flips=flips, min_degree=min_degree, attempts=attempts)
|
||||
if min_degree is not None:
|
||||
min_connectivity = max(min_connectivity, min_degree)
|
||||
G, graph_seed = random_maximal_planar_5_connected(
|
||||
n, seed, flips=flips, min_connectivity=min_connectivity, attempts=attempts)
|
||||
result = dual_cut_random_face(G, rng=rng)
|
||||
result["graph_seed"] = graph_seed
|
||||
result["base_min_degree"] = min(dict(G.degree()).values())
|
||||
result["base_connectivity"] = nx.node_connectivity(G)
|
||||
result["min_degree"] = min(dict(result["G"].degree()).values())
|
||||
result["connectivity"] = nx.node_connectivity(result["G"])
|
||||
return result
|
||||
|
||||
|
||||
@@ -353,7 +352,8 @@ def summary(result):
|
||||
f"(deep embedding of base n="
|
||||
f"{base.number_of_nodes() if base is not None else '?'}) "
|
||||
f"graph_seed={result.get('graph_seed', '?')} "
|
||||
f"min_degree={result.get('min_degree', min(dict(G.degree()).values()))}",
|
||||
f"base_connectivity={result.get('base_connectivity', '?')} "
|
||||
f"deep_connectivity={result.get('connectivity', nx.node_connectivity(G))}",
|
||||
f"chosen face: {result.get('chosen_face', '?')} "
|
||||
f"-> cap vertex x*={result.get('cap_vertex', result['source'])}",
|
||||
f"level source: cap vertex {result['source']} "
|
||||
@@ -828,7 +828,9 @@ def main():
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("-n", type=int, default=20, help="number of vertices")
|
||||
parser.add_argument("--seed", type=int, default=0, help="graph sample seed")
|
||||
parser.add_argument("--min-degree", type=int, default=5)
|
||||
parser.add_argument("--min-connectivity", type=int, default=5)
|
||||
parser.add_argument("--min-degree", type=int, default=None,
|
||||
help="compatibility alias; also raises min-connectivity")
|
||||
parser.add_argument("--face", type=str, default=None,
|
||||
help="fix the chosen face as 'a,b,c' (default: random "
|
||||
"via rng); the deep embedding's cap vertex is the "
|
||||
@@ -846,8 +848,11 @@ def main():
|
||||
|
||||
rng = random.Random(args.seed)
|
||||
if args.face is not None:
|
||||
G, graph_seed = random_maximal_planar_min_degree(
|
||||
args.n, args.seed, min_degree=args.min_degree)
|
||||
min_connectivity = args.min_connectivity
|
||||
if args.min_degree is not None:
|
||||
min_connectivity = max(min_connectivity, args.min_degree)
|
||||
G, graph_seed = random_maximal_planar_5_connected(
|
||||
args.n, args.seed, min_connectivity=min_connectivity)
|
||||
face = tuple(int(x) for x in args.face.split(","))
|
||||
G_prime, cap, depth = deep_embedding(G, face)
|
||||
if args.entry is not None:
|
||||
@@ -860,10 +865,14 @@ def main():
|
||||
result["deep_depth"] = depth
|
||||
result["graph_seed"] = graph_seed
|
||||
result["base_min_degree"] = min(dict(G.degree()).values())
|
||||
result["base_connectivity"] = nx.node_connectivity(G)
|
||||
result["min_degree"] = min(dict(G_prime.degree()).values())
|
||||
result["connectivity"] = nx.node_connectivity(G_prime)
|
||||
else:
|
||||
result = random_dual_cut(n=args.n, seed=args.seed,
|
||||
rng=rng, min_degree=args.min_degree)
|
||||
rng=rng,
|
||||
min_connectivity=args.min_connectivity,
|
||||
min_degree=args.min_degree)
|
||||
|
||||
print(summary(result))
|
||||
if args.png:
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
End-to-end experiment for the *Medial Tire Cuts* paper:
|
||||
|
||||
1. Generate a random maximal planar graph G on n vertices (stacked seed plus
|
||||
random diagonal flips; ``random_maximal_planar`` from the medial tire
|
||||
decompositions experiments), optionally rejecting samples below a requested
|
||||
minimum degree.
|
||||
1. Generate a 5-connected maximal planar graph G on n vertices, using
|
||||
``plantri -c5`` when available and verifying node connectivity.
|
||||
2. Build its medial graph M(G).
|
||||
3. Take the nested tire decomposition at one random vertex level source: the
|
||||
BFS-level treads, each realized as a FullMedialTireGraph.
|
||||
@@ -40,6 +38,7 @@ import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import networkx as nx
|
||||
|
||||
@@ -56,12 +55,125 @@ sys.path.insert(0, _MTD)
|
||||
sys.path.insert(0, _CUT_LIB)
|
||||
|
||||
from tire_realization_analysis import ( # noqa: E402
|
||||
ekey, extract_tread, medial_graph, medial_tire_facemodel,
|
||||
ekey, medial_graph, medial_tire_facemodel,
|
||||
random_maximal_planar, recognise, triangular_faces,
|
||||
)
|
||||
from medial_tire_cut_labelling import door_bite, label_and_cut # noqa: E402
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Component-based tread recognition.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _edge_face_data(faces):
|
||||
edge_faces = defaultdict(list)
|
||||
for i, face in enumerate(faces):
|
||||
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
|
||||
edge_faces[ekey(x, y)].append(i)
|
||||
return edge_faces
|
||||
|
||||
|
||||
def _depth_components(faces, edge_faces, levels):
|
||||
depths = [min(levels[v] for v in face) for face in faces]
|
||||
dual_adj = defaultdict(set)
|
||||
for incident in edge_faces.values():
|
||||
for a in range(len(incident)):
|
||||
for b in range(a + 1, len(incident)):
|
||||
dual_adj[incident[a]].add(incident[b])
|
||||
dual_adj[incident[b]].add(incident[a])
|
||||
|
||||
comps = []
|
||||
seen = [False] * len(faces)
|
||||
for start in range(len(faces)):
|
||||
if seen[start]:
|
||||
continue
|
||||
depth = depths[start]
|
||||
stack = [start]
|
||||
comp = []
|
||||
seen[start] = True
|
||||
while stack:
|
||||
face = stack.pop()
|
||||
comp.append(face)
|
||||
for other in dual_adj[face]:
|
||||
if not seen[other] and depths[other] == depth:
|
||||
seen[other] = True
|
||||
stack.append(other)
|
||||
comps.append((depth, tuple(sorted(comp))))
|
||||
return comps
|
||||
|
||||
|
||||
def _tread_from_component(faces, levels, face_indices):
|
||||
tread_faces = [faces[i] for i in face_indices]
|
||||
if not tread_faces:
|
||||
return None
|
||||
depth = min(min(levels[v] for v in face) for face in tread_faces)
|
||||
annular, up, down = set(), set(), set()
|
||||
face_of_down = defaultdict(int)
|
||||
for face in tread_faces:
|
||||
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
|
||||
e = ekey(x, y)
|
||||
lx, ly = levels[x], levels[y]
|
||||
if {lx, ly} == {depth, depth + 1}:
|
||||
annular.add(e)
|
||||
elif lx == ly == depth:
|
||||
up.add(e)
|
||||
elif lx == ly == depth + 1:
|
||||
down.add(e)
|
||||
face_of_down[e] += 1
|
||||
if len(annular) < 3:
|
||||
return None
|
||||
return {
|
||||
"tread_faces": tread_faces,
|
||||
"annular": annular,
|
||||
"up": up,
|
||||
"down": down,
|
||||
"bites": {e for e in down if face_of_down[e] == 2},
|
||||
}
|
||||
|
||||
|
||||
def _build_treads(faces, levels):
|
||||
"""Recognise simple cycles inside connected depth components.
|
||||
|
||||
The returned ``treads`` keeps the existing simple-tire interface used by
|
||||
the labelling code. ``tread_meta`` records the connected depth component
|
||||
each simple cycle came from, so compound tires can be chained through
|
||||
shared up apexes rather than seeded as unrelated roots.
|
||||
"""
|
||||
treads, skipped, tread_meta = {}, [], {}
|
||||
edge_faces = _edge_face_data(faces)
|
||||
comps = sorted(_depth_components(faces, edge_faces, levels),
|
||||
key=lambda item: (item[0], item[1]))
|
||||
component_count = defaultdict(int)
|
||||
tire_count = defaultdict(int)
|
||||
for depth, face_indices in comps:
|
||||
component = component_count[depth]
|
||||
component_count[depth] += 1
|
||||
tread = _tread_from_component(faces, levels, face_indices)
|
||||
if tread is None:
|
||||
skipped.append(((depth, component), "no tread faces"))
|
||||
continue
|
||||
if len(tread["up"]) < 3:
|
||||
skipped.append(((depth, component), f"only {len(tread['up'])} up teeth"))
|
||||
continue
|
||||
tires = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
|
||||
if not tires:
|
||||
skipped.append(((depth, component), "no annular cycle recognised as a tire"))
|
||||
continue
|
||||
compound = (depth, component)
|
||||
cycle_count = len(tires)
|
||||
for cycle, gb in enumerate(tires):
|
||||
key = (depth, tire_count[depth])
|
||||
tire_count[depth] += 1
|
||||
treads[key] = gb
|
||||
tread_meta[key] = {
|
||||
"compound": compound,
|
||||
"cycle": cycle,
|
||||
"cycle_count": cycle_count,
|
||||
"face_indices": face_indices,
|
||||
}
|
||||
return treads, skipped, tread_meta
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 4. Walk-depth labelling and cut, chained down the tire tree.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -71,7 +183,7 @@ def _apex_vertex(g, bij, edge):
|
||||
return bij[g.apex_of_edge(edge)]
|
||||
|
||||
|
||||
def _label_treads(treads, results, root_entry_edge=None):
|
||||
def _label_treads(treads, results, root_entry_edge=None, tread_meta=None):
|
||||
"""Fill ``results[(d, c)]`` with the walk-depth labelling and cuts for every
|
||||
recognised tire ``c`` of every tread depth ``d``, chaining child entries to
|
||||
parent down teeth.
|
||||
@@ -99,28 +211,85 @@ def _label_treads(treads, results, root_entry_edge=None):
|
||||
if apex not in parent_down or value < parent_down[apex]:
|
||||
parent_down[apex] = value
|
||||
has_parent = any(k[0] == d - 1 for k in treads)
|
||||
for key in sorted(k for k in treads if k[0] == d):
|
||||
pending = sorted(k for k in treads if k[0] == d)
|
||||
while pending:
|
||||
progressed = False
|
||||
deferred = []
|
||||
use_sibling_entries = has_parent and not any(
|
||||
parent_down.keys() & {treads[key][1][f"u{m}"]
|
||||
for m in treads[key][0].up_edges}
|
||||
for key in pending
|
||||
if key not in results
|
||||
)
|
||||
for key in pending:
|
||||
if key in results:
|
||||
continue
|
||||
g, bij = treads[key]
|
||||
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
|
||||
entry = None
|
||||
if not has_parent:
|
||||
if (key == (root_d, 0) and root_entry_edge is not None
|
||||
and root_entry_edge in g.up_edges):
|
||||
entry_edge, start_depth = root_entry_edge, 0
|
||||
entry = (root_entry_edge, 0)
|
||||
else:
|
||||
entry_edge, start_depth = g.up_edges[0], 0 # arbitrary entry
|
||||
entry = (g.up_edges[0], 0) # arbitrary entry
|
||||
else:
|
||||
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
|
||||
best = None
|
||||
for apex, value in parent_down.items():
|
||||
if apex in child_up_apex and (best is None or value < best[1]):
|
||||
best = (child_up_apex[apex], value)
|
||||
if best is not None: # chains to a parent down tooth
|
||||
entry_edge, start_depth = best
|
||||
else: # no shared apex (degenerate); root-style
|
||||
entry = best
|
||||
elif use_sibling_entries:
|
||||
compound = (
|
||||
tread_meta.get(key, {}).get("compound")
|
||||
if tread_meta is not None else None
|
||||
)
|
||||
sibling_best = None
|
||||
if compound is not None:
|
||||
for sk, srec in results.items():
|
||||
if sk[0] != d or srec.get("compound") != compound:
|
||||
continue
|
||||
sg, sbij = srec["g"], srec["bij"]
|
||||
for edge in sg.up_edges:
|
||||
apex = sbij[f"u{edge}"]
|
||||
if apex not in child_up_apex:
|
||||
continue
|
||||
value = srec["depth"][edge] + 1
|
||||
if (sibling_best is None
|
||||
or value > sibling_best[1]):
|
||||
sibling_best = (child_up_apex[apex], value)
|
||||
if sibling_best is not None:
|
||||
entry = sibling_best
|
||||
|
||||
if entry is None:
|
||||
deferred.append(key)
|
||||
continue
|
||||
entry_edge, start_depth = entry
|
||||
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||
"start_depth": start_depth, "depth": depth,
|
||||
"cuts": cuts}
|
||||
if tread_meta is not None and key in tread_meta:
|
||||
results[key].update(tread_meta[key])
|
||||
progressed = True
|
||||
|
||||
if progressed:
|
||||
pending = deferred
|
||||
continue
|
||||
|
||||
# Degenerate component: no parent or labelled sibling gives an
|
||||
# entry. Seed it so any remaining sibling cycles can chain to it.
|
||||
key = deferred[0]
|
||||
g, bij = treads[key]
|
||||
entry_edge, start_depth = g.up_edges[0], 0
|
||||
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||
"start_depth": start_depth, "depth": depth,
|
||||
"cuts": cuts}
|
||||
if tread_meta is not None and key in tread_meta:
|
||||
results[key].update(tread_meta[key])
|
||||
pending = deferred[1:]
|
||||
|
||||
|
||||
def _cap_cut(G, emb, source, levels, results):
|
||||
@@ -252,39 +421,46 @@ def _assemble_cut_graph(M, results, cap_cuts=None):
|
||||
# Driver.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def random_maximal_planar_min_degree(n: int, seed: int, flips: int = 400,
|
||||
min_degree: int = 0,
|
||||
def random_maximal_planar_5_connected(n: int, seed: int, flips: int = 400,
|
||||
min_connectivity: int = 5,
|
||||
attempts: int = 1000) -> tuple[nx.Graph, int]:
|
||||
"""Generate a random maximal planar graph with minimum degree at least
|
||||
``min_degree``. The returned seed is the actual sample seed used."""
|
||||
if min_degree <= 0:
|
||||
"""Generate a maximal planar graph with node connectivity at least
|
||||
``min_connectivity``. The returned seed is the actual sample seed used."""
|
||||
if min_connectivity <= 0:
|
||||
return random_maximal_planar(n, seed, flips=flips), seed
|
||||
|
||||
if min_degree >= 5:
|
||||
if min_connectivity >= 5:
|
||||
plantri = os.path.normpath(os.path.join(_HERE, "..", "..", "..",
|
||||
"plantri", "plantri"))
|
||||
if os.path.exists(plantri):
|
||||
data = subprocess.check_output(
|
||||
[plantri, f"-m{min_degree}", "-g", str(n)],
|
||||
[plantri, "-c5", "-g", str(n)],
|
||||
stderr=subprocess.DEVNULL)
|
||||
graphs = [line for line in data.splitlines() if line]
|
||||
graphs = [
|
||||
line for line in data.splitlines()
|
||||
if line and not line.startswith(b">>")
|
||||
]
|
||||
if graphs:
|
||||
G = nx.from_graph6_bytes(graphs[seed % len(graphs)])
|
||||
return nx.convert_node_labels_to_integers(G), seed
|
||||
for offset in range(len(graphs)):
|
||||
G = nx.from_graph6_bytes(graphs[(seed + offset) % len(graphs)])
|
||||
G = nx.convert_node_labels_to_integers(G)
|
||||
if nx.node_connectivity(G) >= min_connectivity:
|
||||
return G, seed + offset
|
||||
|
||||
for offset in range(attempts):
|
||||
sample_seed = seed + offset
|
||||
G = random_maximal_planar(n, sample_seed, flips=flips)
|
||||
if min(dict(G.degree()).values()) >= min_degree:
|
||||
if nx.node_connectivity(G) >= min_connectivity:
|
||||
return G, sample_seed
|
||||
raise RuntimeError(
|
||||
f"no random maximal planar graph on {n} vertices with "
|
||||
f"minimum degree >= {min_degree} found in {attempts} attempts "
|
||||
f"node connectivity >= {min_connectivity} found in {attempts} attempts "
|
||||
f"starting at seed {seed}")
|
||||
|
||||
|
||||
def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
|
||||
min_degree: int = 5, attempts: int = 1000) -> dict:
|
||||
min_connectivity: int = 5, attempts: int = 1000,
|
||||
min_degree: int | None = None) -> dict:
|
||||
"""Run the full pipeline and return a structured result.
|
||||
|
||||
Result keys: ``n, seed, G, M, source, treads`` (dict depth -> (g, bij)),
|
||||
@@ -292,38 +468,28 @@ def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
|
||||
(depth, reason)), ``cut_graph`` (networkx graph), ``labels`` (list of tooth
|
||||
records), ``warnings``.
|
||||
"""
|
||||
G, graph_seed = random_maximal_planar_min_degree(
|
||||
n, seed, flips=flips, min_degree=min_degree, attempts=attempts)
|
||||
if min_degree is not None:
|
||||
min_connectivity = max(min_connectivity, min_degree)
|
||||
G, graph_seed = random_maximal_planar_5_connected(
|
||||
n, seed, flips=flips, min_connectivity=min_connectivity, attempts=attempts)
|
||||
faces, emb = triangular_faces(G)
|
||||
M = medial_graph(G)
|
||||
source = random.Random(f"source-{graph_seed}").choice(sorted(G.nodes()))
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
|
||||
treads, skipped = {}, []
|
||||
for d in range(max(levels.values())):
|
||||
tread = extract_tread(faces, levels, d)
|
||||
if tread is None:
|
||||
skipped.append((d, "no tread faces"))
|
||||
continue
|
||||
if len(tread["up"]) < 3:
|
||||
skipped.append((d, f"only {len(tread['up'])} up teeth"))
|
||||
continue
|
||||
tires = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
|
||||
if not tires:
|
||||
skipped.append((d, "no annular cycle recognised as a tire"))
|
||||
continue
|
||||
for c, gb in enumerate(tires):
|
||||
treads[(d, c)] = gb
|
||||
treads, skipped, tread_meta = _build_treads(faces, levels)
|
||||
|
||||
results = {}
|
||||
_label_treads(treads, results)
|
||||
_label_treads(treads, results, tread_meta=tread_meta)
|
||||
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
||||
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
||||
return {
|
||||
"n": n, "seed": seed, "graph_seed": graph_seed,
|
||||
"min_degree": min(dict(G.degree()).values()),
|
||||
"connectivity": nx.node_connectivity(G),
|
||||
"G": G, "M": M, "source": source,
|
||||
"treads": treads, "results": results, "cap_cuts": cap_cuts,
|
||||
"treads": treads, "tread_meta": tread_meta,
|
||||
"results": results, "cap_cuts": cap_cuts,
|
||||
"skipped": skipped,
|
||||
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
|
||||
}
|
||||
@@ -370,6 +536,7 @@ def to_json(result: dict) -> dict:
|
||||
return {
|
||||
"n": result["n"], "seed": result["seed"],
|
||||
"graph_seed": result["graph_seed"], "min_degree": result["min_degree"],
|
||||
"connectivity": result["connectivity"],
|
||||
"source": result["source"],
|
||||
"graph_edges": sorted([int(u), int(v)] for u, v in result["G"].edges()),
|
||||
"medial_vertices": result["M"].number_of_nodes(),
|
||||
@@ -397,7 +564,8 @@ def summary(result: dict) -> str:
|
||||
lines = [
|
||||
f"random maximal planar graph: n={result['n']} requested seed={result['seed']} "
|
||||
f"graph seed={result['graph_seed']} "
|
||||
f"({result['G'].number_of_edges()} edges, min degree {result['min_degree']})",
|
||||
f"({result['G'].number_of_edges()} edges, "
|
||||
f"connectivity {result['connectivity']}, min degree {result['min_degree']})",
|
||||
f"medial graph M(G): {result['M'].number_of_nodes()} vertices",
|
||||
f"level source: vertex {result['source']}",
|
||||
f"recognised tires (depth, component): {sorted(res)}",
|
||||
@@ -427,16 +595,20 @@ def main() -> None:
|
||||
parser.add_argument("--seed", type=int, default=0)
|
||||
parser.add_argument("--flips", type=int, default=400,
|
||||
help="number of random diagonal flips when building G")
|
||||
parser.add_argument("--min-degree", type=int, default=5,
|
||||
help="reject random graphs below this minimum degree")
|
||||
parser.add_argument("--min-connectivity", type=int, default=5,
|
||||
help="reject random graphs below this node connectivity")
|
||||
parser.add_argument("--min-degree", type=int, default=None,
|
||||
help="compatibility alias; also raises min-connectivity")
|
||||
parser.add_argument("--attempts", type=int, default=1000,
|
||||
help="number of consecutive seeds to try for --min-degree")
|
||||
help="number of consecutive seeds to try for sampling")
|
||||
parser.add_argument("--json", metavar="PATH",
|
||||
help="write the full result as JSON to PATH")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = run_experiment(n=args.n, seed=args.seed, flips=args.flips,
|
||||
min_degree=args.min_degree, attempts=args.attempts)
|
||||
min_connectivity=args.min_connectivity,
|
||||
min_degree=args.min_degree,
|
||||
attempts=args.attempts)
|
||||
print(summary(result))
|
||||
if args.json:
|
||||
with open(args.json, "w") as fh:
|
||||
|
||||
@@ -205,7 +205,8 @@ def label_and_cut(graph: FullMedialTireGraph, entry_edge: int,
|
||||
|
||||
|
||||
def up_apex_cuts(graph: FullMedialTireGraph, entry_edge: int,
|
||||
bij: Mapping[str, object] | None = None) -> dict[int, object]:
|
||||
bij: Mapping[str, object] | None = None,
|
||||
shared_apexes: set[object] | None = None) -> dict[int, object]:
|
||||
"""Up-tooth apex cuts prescribed after the walk-depth traversal.
|
||||
|
||||
The returned dict maps each cut up-tooth edge to the apex vertex to
|
||||
@@ -218,11 +219,12 @@ def up_apex_cuts(graph: FullMedialTireGraph, entry_edge: int,
|
||||
i: (bij[f"u{i}"] if bij is not None else graph.apex_of_edge(i))
|
||||
for i in graph.up_edges
|
||||
}
|
||||
shared_apexes = shared_apexes or set()
|
||||
multiplicity = Counter(apex_by_edge.values())
|
||||
return {
|
||||
i: apex
|
||||
for i, apex in apex_by_edge.items()
|
||||
if i != entry_edge and multiplicity[apex] == 1
|
||||
if i != entry_edge and multiplicity[apex] == 1 and apex not in shared_apexes
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -812,7 +812,10 @@ def run(args: argparse.Namespace):
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.graph6:
|
||||
graphs = [nx.from_graph6_bytes(args.graph6.encode())]
|
||||
graph = nx.from_graph6_bytes(args.graph6.encode())
|
||||
if nx.node_connectivity(graph) < 5:
|
||||
raise ValueError("--graph6 graph must be 5-connected")
|
||||
graphs = [graph]
|
||||
if args.source is None:
|
||||
raise ValueError("--source is required with --graph6")
|
||||
sources = [args.source]
|
||||
|
||||
Reference in New Issue
Block a user