Handle compound medial tires in cut labelling

This commit is contained in:
2026-06-15 21:46:56 -04:00
parent 37a7ff0b00
commit 1e8bee04ce
5 changed files with 296 additions and 110 deletions
@@ -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):
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:
@@ -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
}
@@ -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]