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)
|
sys.path.insert(0, _CUT_LIB)
|
||||||
|
|
||||||
from tire_realization_analysis import ( # noqa: E402
|
from tire_realization_analysis import ( # noqa: E402
|
||||||
ekey, extract_tread, medial_graph, medial_tire_facemodel,
|
ekey, medial_graph, triangular_faces,
|
||||||
recognise, triangular_faces,
|
|
||||||
)
|
)
|
||||||
from run_medial_tire_cut_experiment import ( # noqa: E402
|
from run_medial_tire_cut_experiment import ( # noqa: E402
|
||||||
_assemble_cut_graph, _cap_cut, _label_treads,
|
_assemble_cut_graph, _build_treads, _cap_cut, _label_treads,
|
||||||
random_maximal_planar_min_degree,
|
random_maximal_planar_5_connected,
|
||||||
)
|
)
|
||||||
from medial_tire_cut_labelling import up_apex_cuts # noqa: E402
|
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.
|
# 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):
|
def root_entry_choices(G, source):
|
||||||
"""Edge indices of the root tread's up teeth -- the eligible entry teeth.
|
"""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)
|
faces, _ = triangular_faces(G)
|
||||||
levels = nx.single_source_shortest_path_length(G, source)
|
levels = nx.single_source_shortest_path_length(G, source)
|
||||||
treads, _ = _build_treads(faces, levels)
|
treads, _skipped, _meta = _build_treads(faces, levels)
|
||||||
if not treads:
|
if not treads:
|
||||||
return []
|
return []
|
||||||
g, _bij = treads[min(treads)]
|
g, _bij = treads[min(treads)]
|
||||||
@@ -209,12 +183,28 @@ def annular_cut_edges(results, cap_cuts):
|
|||||||
def up_apex_cut_edges(results):
|
def up_apex_cut_edges(results):
|
||||||
"""Primal edges whose dual edge the apex duplications remove: the apex
|
"""Primal edges whose dual edge the apex duplications remove: the apex
|
||||||
medial vertex of every up tooth across all treads, except each tread's
|
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()
|
removed = set()
|
||||||
for key in sorted(results):
|
for key in sorted(results):
|
||||||
g, bij = results[key]["g"], results[key]["bij"]
|
rec = results[key]
|
||||||
entry = results[key]["entry_edge"]
|
g, bij = rec["g"], rec["bij"]
|
||||||
removed.update(up_apex_cuts(g, entry, bij=bij).values())
|
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
|
return removed
|
||||||
|
|
||||||
|
|
||||||
@@ -245,7 +235,7 @@ def medial_tire_dual_cut(G, source, entry_edge):
|
|||||||
faces, emb = triangular_faces(G)
|
faces, emb = triangular_faces(G)
|
||||||
M = medial_graph(G)
|
M = medial_graph(G)
|
||||||
levels = nx.single_source_shortest_path_length(G, source)
|
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:
|
if not treads:
|
||||||
raise ValueError(f"level source {source} induces no recognised tread")
|
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)})")
|
f"(choices: {sorted(g_root.up_edges)})")
|
||||||
|
|
||||||
results = {}
|
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)
|
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
||||||
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
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,
|
"G": G, "M": M, "source": source, "entry_edge": entry_edge,
|
||||||
"entry_medial_vertex": entry_medial,
|
"entry_medial_vertex": entry_medial,
|
||||||
"faces": faces, "outer_face": 0,
|
"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,
|
"results": results, "cap_cuts": cap_cuts, "cut_graph": cut_graph,
|
||||||
"labels": labels, "warnings": warnings,
|
"labels": labels, "warnings": warnings,
|
||||||
"dual": dual, "removed_dual_edges": removed,
|
"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")
|
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):
|
def random_dual_cut(
|
||||||
"""Find a random maximal planar graph of minimum degree ``min_degree``, then
|
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``.
|
``dual_cut_random_face``.
|
||||||
|
|
||||||
``seed`` drives the graph sample; ``rng`` (defaulting to ``Random(seed)``)
|
``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)``.
|
pipeline is reproducible from ``(n, seed)``.
|
||||||
"""
|
"""
|
||||||
rng = rng or random.Random(seed)
|
rng = rng or random.Random(seed)
|
||||||
G, graph_seed = random_maximal_planar_min_degree(
|
if min_degree is not None:
|
||||||
n, seed, flips=flips, min_degree=min_degree, attempts=attempts)
|
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 = dual_cut_random_face(G, rng=rng)
|
||||||
result["graph_seed"] = graph_seed
|
result["graph_seed"] = graph_seed
|
||||||
result["base_min_degree"] = min(dict(G.degree()).values())
|
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["min_degree"] = min(dict(result["G"].degree()).values())
|
||||||
|
result["connectivity"] = nx.node_connectivity(result["G"])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -353,7 +352,8 @@ def summary(result):
|
|||||||
f"(deep embedding of base n="
|
f"(deep embedding of base n="
|
||||||
f"{base.number_of_nodes() if base is not None else '?'}) "
|
f"{base.number_of_nodes() if base is not None else '?'}) "
|
||||||
f"graph_seed={result.get('graph_seed', '?')} "
|
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"chosen face: {result.get('chosen_face', '?')} "
|
||||||
f"-> cap vertex x*={result.get('cap_vertex', result['source'])}",
|
f"-> cap vertex x*={result.get('cap_vertex', result['source'])}",
|
||||||
f"level source: cap vertex {result['source']} "
|
f"level source: cap vertex {result['source']} "
|
||||||
@@ -828,7 +828,9 @@ def main():
|
|||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument("-n", type=int, default=20, help="number of vertices")
|
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("--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,
|
parser.add_argument("--face", type=str, default=None,
|
||||||
help="fix the chosen face as 'a,b,c' (default: random "
|
help="fix the chosen face as 'a,b,c' (default: random "
|
||||||
"via rng); the deep embedding's cap vertex is the "
|
"via rng); the deep embedding's cap vertex is the "
|
||||||
@@ -846,8 +848,11 @@ def main():
|
|||||||
|
|
||||||
rng = random.Random(args.seed)
|
rng = random.Random(args.seed)
|
||||||
if args.face is not None:
|
if args.face is not None:
|
||||||
G, graph_seed = random_maximal_planar_min_degree(
|
min_connectivity = args.min_connectivity
|
||||||
args.n, args.seed, min_degree=args.min_degree)
|
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(","))
|
face = tuple(int(x) for x in args.face.split(","))
|
||||||
G_prime, cap, depth = deep_embedding(G, face)
|
G_prime, cap, depth = deep_embedding(G, face)
|
||||||
if args.entry is not None:
|
if args.entry is not None:
|
||||||
@@ -860,10 +865,14 @@ def main():
|
|||||||
result["deep_depth"] = depth
|
result["deep_depth"] = depth
|
||||||
result["graph_seed"] = graph_seed
|
result["graph_seed"] = graph_seed
|
||||||
result["base_min_degree"] = min(dict(G.degree()).values())
|
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["min_degree"] = min(dict(G_prime.degree()).values())
|
||||||
|
result["connectivity"] = nx.node_connectivity(G_prime)
|
||||||
else:
|
else:
|
||||||
result = random_dual_cut(n=args.n, seed=args.seed,
|
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))
|
print(summary(result))
|
||||||
if args.png:
|
if args.png:
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
End-to-end experiment for the *Medial Tire Cuts* paper:
|
End-to-end experiment for the *Medial Tire Cuts* paper:
|
||||||
|
|
||||||
1. Generate a random maximal planar graph G on n vertices (stacked seed plus
|
1. Generate a 5-connected maximal planar graph G on n vertices, using
|
||||||
random diagonal flips; ``random_maximal_planar`` from the medial tire
|
``plantri -c5`` when available and verifying node connectivity.
|
||||||
decompositions experiments), optionally rejecting samples below a requested
|
|
||||||
minimum degree.
|
|
||||||
2. Build its medial graph M(G).
|
2. Build its medial graph M(G).
|
||||||
3. Take the nested tire decomposition at one random vertex level source: the
|
3. Take the nested tire decomposition at one random vertex level source: the
|
||||||
BFS-level treads, each realized as a FullMedialTireGraph.
|
BFS-level treads, each realized as a FullMedialTireGraph.
|
||||||
@@ -40,6 +38,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
@@ -56,12 +55,125 @@ sys.path.insert(0, _MTD)
|
|||||||
sys.path.insert(0, _CUT_LIB)
|
sys.path.insert(0, _CUT_LIB)
|
||||||
|
|
||||||
from tire_realization_analysis import ( # noqa: E402
|
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,
|
random_maximal_planar, recognise, triangular_faces,
|
||||||
)
|
)
|
||||||
from medial_tire_cut_labelling import door_bite, label_and_cut # noqa: E402
|
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.
|
# 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)]
|
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
|
"""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
|
recognised tire ``c`` of every tread depth ``d``, chaining child entries to
|
||||||
parent down teeth.
|
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]:
|
if apex not in parent_down or value < parent_down[apex]:
|
||||||
parent_down[apex] = value
|
parent_down[apex] = value
|
||||||
has_parent = any(k[0] == d - 1 for k in treads)
|
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)
|
||||||
g, bij = treads[key]
|
while pending:
|
||||||
if not has_parent:
|
progressed = False
|
||||||
if (key == (root_d, 0) and root_entry_edge is not None
|
deferred = []
|
||||||
and root_entry_edge in g.up_edges):
|
use_sibling_entries = has_parent and not any(
|
||||||
entry_edge, start_depth = root_entry_edge, 0
|
parent_down.keys() & {treads[key][1][f"u{m}"]
|
||||||
else:
|
for m in treads[key][0].up_edges}
|
||||||
entry_edge, start_depth = g.up_edges[0], 0 # arbitrary entry
|
for key in pending
|
||||||
else:
|
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}
|
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
|
||||||
best = None
|
entry = None
|
||||||
for apex, value in parent_down.items():
|
if not has_parent:
|
||||||
if apex in child_up_apex and (best is None or value < best[1]):
|
if (key == (root_d, 0) and root_entry_edge is not None
|
||||||
best = (child_up_apex[apex], value)
|
and root_entry_edge in g.up_edges):
|
||||||
if best is not None: # chains to a parent down tooth
|
entry = (root_entry_edge, 0)
|
||||||
entry_edge, start_depth = best
|
else:
|
||||||
else: # no shared apex (degenerate); root-style
|
entry = (g.up_edges[0], 0) # arbitrary entry
|
||||||
entry_edge, start_depth = g.up_edges[0], 0
|
else:
|
||||||
|
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 = 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)
|
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||||
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||||
"start_depth": start_depth, "depth": depth,
|
"start_depth": start_depth, "depth": depth,
|
||||||
"cuts": cuts}
|
"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):
|
def _cap_cut(G, emb, source, levels, results):
|
||||||
@@ -252,39 +421,46 @@ def _assemble_cut_graph(M, results, cap_cuts=None):
|
|||||||
# Driver.
|
# Driver.
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def random_maximal_planar_min_degree(n: int, seed: int, flips: int = 400,
|
def random_maximal_planar_5_connected(n: int, seed: int, flips: int = 400,
|
||||||
min_degree: int = 0,
|
min_connectivity: int = 5,
|
||||||
attempts: int = 1000) -> tuple[nx.Graph, int]:
|
attempts: int = 1000) -> tuple[nx.Graph, int]:
|
||||||
"""Generate a random maximal planar graph with minimum degree at least
|
"""Generate a maximal planar graph with node connectivity at least
|
||||||
``min_degree``. The returned seed is the actual sample seed used."""
|
``min_connectivity``. The returned seed is the actual sample seed used."""
|
||||||
if min_degree <= 0:
|
if min_connectivity <= 0:
|
||||||
return random_maximal_planar(n, seed, flips=flips), seed
|
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 = os.path.normpath(os.path.join(_HERE, "..", "..", "..",
|
||||||
"plantri", "plantri"))
|
"plantri", "plantri"))
|
||||||
if os.path.exists(plantri):
|
if os.path.exists(plantri):
|
||||||
data = subprocess.check_output(
|
data = subprocess.check_output(
|
||||||
[plantri, f"-m{min_degree}", "-g", str(n)],
|
[plantri, "-c5", "-g", str(n)],
|
||||||
stderr=subprocess.DEVNULL)
|
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:
|
if graphs:
|
||||||
G = nx.from_graph6_bytes(graphs[seed % len(graphs)])
|
for offset in range(len(graphs)):
|
||||||
return nx.convert_node_labels_to_integers(G), seed
|
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):
|
for offset in range(attempts):
|
||||||
sample_seed = seed + offset
|
sample_seed = seed + offset
|
||||||
G = random_maximal_planar(n, sample_seed, flips=flips)
|
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
|
return G, sample_seed
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"no random maximal planar graph on {n} vertices with "
|
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}")
|
f"starting at seed {seed}")
|
||||||
|
|
||||||
|
|
||||||
def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
|
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.
|
"""Run the full pipeline and return a structured result.
|
||||||
|
|
||||||
Result keys: ``n, seed, G, M, source, treads`` (dict depth -> (g, bij)),
|
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
|
(depth, reason)), ``cut_graph`` (networkx graph), ``labels`` (list of tooth
|
||||||
records), ``warnings``.
|
records), ``warnings``.
|
||||||
"""
|
"""
|
||||||
G, graph_seed = random_maximal_planar_min_degree(
|
if min_degree is not None:
|
||||||
n, seed, flips=flips, min_degree=min_degree, attempts=attempts)
|
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)
|
faces, emb = triangular_faces(G)
|
||||||
M = medial_graph(G)
|
M = medial_graph(G)
|
||||||
source = random.Random(f"source-{graph_seed}").choice(sorted(G.nodes()))
|
source = random.Random(f"source-{graph_seed}").choice(sorted(G.nodes()))
|
||||||
levels = nx.single_source_shortest_path_length(G, source)
|
levels = nx.single_source_shortest_path_length(G, source)
|
||||||
|
|
||||||
treads, skipped = {}, []
|
treads, skipped, tread_meta = _build_treads(faces, levels)
|
||||||
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
|
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
_label_treads(treads, results)
|
_label_treads(treads, results, tread_meta=tread_meta)
|
||||||
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
||||||
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
||||||
return {
|
return {
|
||||||
"n": n, "seed": seed, "graph_seed": graph_seed,
|
"n": n, "seed": seed, "graph_seed": graph_seed,
|
||||||
"min_degree": min(dict(G.degree()).values()),
|
"min_degree": min(dict(G.degree()).values()),
|
||||||
|
"connectivity": nx.node_connectivity(G),
|
||||||
"G": G, "M": M, "source": source,
|
"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,
|
"skipped": skipped,
|
||||||
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
|
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
|
||||||
}
|
}
|
||||||
@@ -370,6 +536,7 @@ def to_json(result: dict) -> dict:
|
|||||||
return {
|
return {
|
||||||
"n": result["n"], "seed": result["seed"],
|
"n": result["n"], "seed": result["seed"],
|
||||||
"graph_seed": result["graph_seed"], "min_degree": result["min_degree"],
|
"graph_seed": result["graph_seed"], "min_degree": result["min_degree"],
|
||||||
|
"connectivity": result["connectivity"],
|
||||||
"source": result["source"],
|
"source": result["source"],
|
||||||
"graph_edges": sorted([int(u), int(v)] for u, v in result["G"].edges()),
|
"graph_edges": sorted([int(u), int(v)] for u, v in result["G"].edges()),
|
||||||
"medial_vertices": result["M"].number_of_nodes(),
|
"medial_vertices": result["M"].number_of_nodes(),
|
||||||
@@ -397,7 +564,8 @@ def summary(result: dict) -> str:
|
|||||||
lines = [
|
lines = [
|
||||||
f"random maximal planar graph: n={result['n']} requested seed={result['seed']} "
|
f"random maximal planar graph: n={result['n']} requested seed={result['seed']} "
|
||||||
f"graph seed={result['graph_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"medial graph M(G): {result['M'].number_of_nodes()} vertices",
|
||||||
f"level source: vertex {result['source']}",
|
f"level source: vertex {result['source']}",
|
||||||
f"recognised tires (depth, component): {sorted(res)}",
|
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("--seed", type=int, default=0)
|
||||||
parser.add_argument("--flips", type=int, default=400,
|
parser.add_argument("--flips", type=int, default=400,
|
||||||
help="number of random diagonal flips when building G")
|
help="number of random diagonal flips when building G")
|
||||||
parser.add_argument("--min-degree", type=int, default=5,
|
parser.add_argument("--min-connectivity", type=int, default=5,
|
||||||
help="reject random graphs below this minimum degree")
|
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,
|
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",
|
parser.add_argument("--json", metavar="PATH",
|
||||||
help="write the full result as JSON to PATH")
|
help="write the full result as JSON to PATH")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
result = run_experiment(n=args.n, seed=args.seed, flips=args.flips,
|
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))
|
print(summary(result))
|
||||||
if args.json:
|
if args.json:
|
||||||
with open(args.json, "w") as fh:
|
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,
|
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.
|
"""Up-tooth apex cuts prescribed after the walk-depth traversal.
|
||||||
|
|
||||||
The returned dict maps each cut up-tooth edge to the apex vertex to
|
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))
|
i: (bij[f"u{i}"] if bij is not None else graph.apex_of_edge(i))
|
||||||
for i in graph.up_edges
|
for i in graph.up_edges
|
||||||
}
|
}
|
||||||
|
shared_apexes = shared_apexes or set()
|
||||||
multiplicity = Counter(apex_by_edge.values())
|
multiplicity = Counter(apex_by_edge.values())
|
||||||
return {
|
return {
|
||||||
i: apex
|
i: apex
|
||||||
for i, apex in apex_by_edge.items()
|
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)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if args.graph6:
|
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:
|
if args.source is None:
|
||||||
raise ValueError("--source is required with --graph6")
|
raise ValueError("--source is required with --graph6")
|
||||||
sources = [args.source]
|
sources = [args.source]
|
||||||
|
|||||||
Reference in New Issue
Block a user