"""Draw the walk-depth labelling and cut of a medial tire decomposition. Paper-graphics companion to ``run_medial_tire_cut_experiment.py``: it imports ``run_experiment`` from there, runs the pipeline on a random maximal planar graph, and emits TikZ. By default it draws one ``tikzpicture`` (walk-depth labels + cut slits) per recognised full medial tire graph, using ``to_tikz`` from ``medial_tire_cut_labelling``. With ``--whole`` it instead draws the whole medial graph M(G) with every tire's cuts applied, on a Kamada--Kawai layout, the recognised tires highlighted and the rest of M(G) in grey. This script only renders; the experiment itself draws nothing. Run with the repo venv (networkx): ``.venv/bin/python``. Examples: .venv/bin/python draw_medial_tire_cut.py -n 20 --seed 72 > panels.tex .venv/bin/python draw_medial_tire_cut.py -n 20 --seed 72 --whole > whole.tex """ from __future__ import annotations import argparse import math import os import sys import networkx as nx _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) from run_medial_tire_cut_experiment import run_experiment # noqa: E402 from medial_tire_cut_labelling import to_tikz # noqa: E402 def tikz_panels(n: int, seed: int, scale: float = 1.6) -> tuple[dict, list[str]]: """Run the experiment and return ``(result, panels)``, one TikZ panel per recognised tread, each showing that tread's walk-depth labelling and cut.""" result = run_experiment(n=n, seed=seed) panels = [] for d in sorted(result["results"]): rec = result["results"][d] panels.append(to_tikz(rec["g"], depth=rec["depth"], cuts=rec["cuts"], entry_edge=rec["entry_edge"], scale=scale)) return result, panels # --------------------------------------------------------------------------- # # The whole medial graph: M(G) with all tire cuts applied. # --------------------------------------------------------------------------- # def _is_split(node) -> bool: return isinstance(node, tuple) and len(node) == 3 and node[1] in ("A", "B") def _medial_layout(H: nx.Graph) -> dict: """A Kamada--Kawai layout of the (planar) cut graph, normalised to the unit box. The two copies of a cut vertex have different neighbours, so the layout separates them automatically, showing the slit.""" pos = nx.kamada_kawai_layout(H) xs = [p[0] for p in pos.values()] ys = [p[1] for p in pos.values()] cx, cy = 0.5 * (max(xs) + min(xs)), 0.5 * (max(ys) + min(ys)) span = max(max(xs) - min(xs), max(ys) - min(ys)) or 1.0 return {v: ((p[0] - cx) / span, (p[1] - cy) / span) for v, p in pos.items()} def medial_tikz(result: dict, scale: float = 9.0) -> str: """A TikZ ``tikzpicture`` of the whole medial graph M(G) with every tire's cuts applied. Tire teeth are coloured and carry their walk depth; annular medial vertices are black; medial vertices outside any recognised tire are grey; cut (split) vertices are drawn as separated copies.""" H = result["cut_graph"] pos = _medial_layout(H) # role of each medial vertex: annular / up / down / bite, and walk depth. annular = set() for d in sorted(result["results"]): g, bij = result["results"][d]["g"], result["results"][d]["bij"] annular.update(bij[f"a{k}"] for k in range(g.n)) apex = {r["apex"]: (r["role"], r["walk"]) for r in result["labels"]} def edge_of(node): return node[0] if _is_split(node) else node L = [] A = L.append A(f"\\begin{{tikzpicture}}[scale={scale},") A(" med/.style={black!30, line width=0.3pt},") A(" grey/.style={circle, draw=black!45, fill=black!8, inner sep=0.9pt},") A(" ann/.style={circle, fill=black, inner sep=1.0pt},") A(" cutv/.style={circle, draw=red!75!black, fill=red!12, inner sep=1.0pt},") A(" upv/.style={circle, draw=blue!70!black, fill=blue!15, inner sep=1.3pt},") A(" downv/.style={circle, draw=red!70!black, fill=red!15, inner sep=1.3pt},") A(" bitev/.style={circle, draw=red!70!black, fill=red!35, inner sep=1.6pt},") A(" dlbl/.style={font=\\tiny\\bfseries, text=black, inner sep=0.5pt}]") def pt(node): x, y = pos[node] return f"({x:.3f},{y:.3f})" for u, v in H.edges(): A(f"\\draw[med] {pt(u)}--{pt(v)};") for node in H.nodes(): mv = edge_of(node) if mv in apex: role, _ = apex[mv] style = {"up": "upv", "down": "downv", "bite": "bitev"}[role] elif mv in annular: style = "cutv" if _is_split(node) else "ann" else: style = "grey" A(f"\\node[{style}] at {pt(node)} {{}};") for node in H.nodes(): mv = edge_of(node) if _is_split(node) or mv not in apex: continue x, y = pos[node] A(f"\\node[dlbl] at ({x:.3f},{y:.3f}) [yshift=4.5pt] {{{apex[mv][1]}}};") A("\\end{tikzpicture}") return "\n".join(L) def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-n", type=int, default=20) parser.add_argument("--seed", type=int, default=72) parser.add_argument("--scale", type=float, default=1.6) parser.add_argument("--whole", action="store_true", help="draw the whole medial graph M(G) with all cuts, " "instead of one panel per tread") args = parser.parse_args() if args.whole: result = run_experiment(n=args.n, seed=args.seed) treads = sorted(result["results"]) print(f"% whole medial graph: n={args.n} seed={args.seed} " f"source={result['source']} recognised treads={treads} " f"|M(G)|={result['M'].number_of_nodes()}") print(medial_tikz(result, scale=args.scale if args.scale != 1.6 else 9.0)) return result, panels = tikz_panels(args.n, args.seed, scale=args.scale) treads = sorted(result["results"]) print(f"% medial tire cut: n={args.n} seed={args.seed} " f"source={result['source']} recognised treads={treads}") if not panels: print("% (no recognised full medial tire graphs for this graph)") for d, panel in zip(treads, panels): g = result["results"][d]["g"] print(f"% --- tread {d}: |A(T)|={g.n} word={g.tooth_word} " f"bites={sorted(g.bites)} ---") print(panel) if __name__ == "__main__": main()