From 1e8bee04ce9238773f064786f24e66835e0f3628 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 15 Jun 2026 21:46:56 -0400 Subject: [PATCH] Handle compound medial tires in cut labelling --- .../full_walk/full_medial_tire_cut_walk_1.pdf | Bin 65159 -> 65159 bytes .../medial_tire_dual_cut_experiment.py | 101 +++--- .../run_medial_tire_cut_experiment.py | 294 ++++++++++++++---- .../lib/medial_tire_cut_labelling.py | 6 +- .../draw_random_medial_tire_decompositions.py | 5 +- 5 files changed, 296 insertions(+), 110 deletions(-) diff --git a/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.pdf b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.pdf index 7a3510f9d7b194083557b4eec191757d0a3d5791..935bb1811230f41739b78eadf3a2d0839c19cfbf 100644 GIT binary patch delta 20 bcmZqw%iR8#c|-blRznjr1B1;u-=*0BVP*(b delta 20 bcmZqw%iR8#c|-blRs$1r6Z6eE-=*0BVX6pV diff --git a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py index 6925402..391e6e6 100644 --- a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py +++ b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py @@ -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: diff --git a/papers/medial_tire_cuts/experiments/run_medial_tire_cut_experiment.py b/papers/medial_tire_cuts/experiments/run_medial_tire_cut_experiment.py index 6ccbb71..efcc86e 100644 --- a/papers/medial_tire_cuts/experiments/run_medial_tire_cut_experiment.py +++ b/papers/medial_tire_cuts/experiments/run_medial_tire_cut_experiment.py @@ -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): - g, bij = treads[key] - 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 - else: - entry_edge, start_depth = g.up_edges[0], 0 # arbitrary entry - else: + 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} - 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_edge, start_depth = g.up_edges[0], 0 + 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 = (root_entry_edge, 0) + else: + entry = (g.up_edges[0], 0) # arbitrary entry + 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) 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, - 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: +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 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: diff --git a/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py b/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py index 83ffb33..81a80a5 100644 --- a/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py +++ b/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py @@ -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 } diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/lib/draw_random_medial_tire_decompositions.py b/papers/medial_tire_decompositions_of_plane_triangulations/lib/draw_random_medial_tire_decompositions.py index 10b62f0..816b7d1 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/lib/draw_random_medial_tire_decompositions.py +++ b/papers/medial_tire_decompositions_of_plane_triangulations/lib/draw_random_medial_tire_decompositions.py @@ -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]