"""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 a two-panel Figure 3 graphic: the source graph with its source highlighted and the whole medial graph M(G) drawn with every medial vertex at the midpoint of its source edge and labelled by that source edge, with the full BFS-level chain shown and the currently computed walk-depth labels and cuts marked. 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 59 > panels.tex .venv/bin/python draw_medial_tire_cut.py -n 20 --seed 59 --whole > whole.tex """ from __future__ import annotations import argparse import math import os import sys import networkx as nx import numpy as np _HERE = os.path.dirname(os.path.abspath(__file__)) _PAPER_DIR = os.path.dirname(_HERE) _CUT_LIB = os.path.join(_PAPER_DIR, "lib") sys.path.insert(0, _HERE) sys.path.insert(0, _CUT_LIB) from run_medial_tire_cut_experiment import run_experiment # noqa: E402 from medial_tire_cut_labelling import to_tikz # noqa: E402 from tire_realization_analysis import triangular_faces # noqa: E402 def tikz_panels(n: int, seed: int, scale: float = 1.6, min_degree: int = 5, attempts: int = 1000) -> 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, min_degree=min_degree, attempts=attempts) 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 # --------------------------------------------------------------------------- # # Figure 3: the source graph and midpoint drawing of the whole medial graph. # --------------------------------------------------------------------------- # def _source_layout(G: nx.Graph) -> dict[int, tuple[float, float]]: """Straight-line planar layout for the source graph, normalised to the unit box and reused by the medial drawing.""" faces, _ = triangular_faces(G) outer = list(faces[0]) outer_set = set(outer) raw = {} for i, v in enumerate(outer): angle = math.radians(90.0 - i * 360.0 / len(outer)) raw[v] = np.array([math.cos(angle), math.sin(angle)], dtype=float) inner = [v for v in sorted(G.nodes()) if v not in outer_set] if inner: idx = {v: i for i, v in enumerate(inner)} n = len(inner) A = np.zeros((n, n)) bx = np.zeros(n) by = np.zeros(n) for i, v in enumerate(inner): nbrs = list(G.neighbors(v)) A[i, i] = 1.0 for w in nbrs: if w in idx: A[i, idx[w]] -= 1.0 / len(nbrs) else: bx[i] += raw[w][0] / len(nbrs) by[i] += raw[w][1] / len(nbrs) xs = np.linalg.solve(A, bx) ys = np.linalg.solve(A, by) for v in inner: raw[v] = np.array([xs[idx[v]], ys[idx[v]]], dtype=float) pts = np.array([raw[v] for v in G.nodes()], dtype=float) center = 0.5 * (pts.max(axis=0) + pts.min(axis=0)) span = float(max(*(pts.max(axis=0) - pts.min(axis=0)), 1.0)) return { v: tuple((raw[v] - center) / span) for v in G.nodes() } def _edge_midpoint(pos: dict, edge) -> tuple[float, float]: u, v = edge ux, uy = pos[u] vx, vy = pos[v] return (0.5 * (ux + vx), 0.5 * (uy + vy)) def _edge_label(edge) -> str: u, v = edge return f"${u}\\!{{-}}\\!{v}$" def _source_graph_tikz(result: dict, pos: dict, scale: float) -> str: G, source = result["G"], result["source"] L = [] A = L.append A(f"\\begin{{tikzpicture}}[scale={scale},") A(" sedge/.style={black!50, line width=0.35pt},") A(" sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},") A(" srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]") def pt(v): x, y = pos[v] return f"({x:.3f},{y:.3f})" for u, v in sorted(G.edges()): A(f"\\draw[sedge] {pt(u)}--{pt(v)};") for v in sorted(G.nodes()): style = "srcv" if v == source else "sv" A(f"\\node[{style}] at {pt(v)} {{}};") sx, sy = pos[source] A(f"\\node[font=\\scriptsize, text=blue!70!black] at ({sx:.3f},{sy - 0.085:.3f}) {{source {source}}};") A("\\end{tikzpicture}") return "\n".join(L) def _medial_midpoint_tikz(result: dict, pos: dict, scale: float) -> str: """Draw M(G) with each medial vertex at the midpoint of its source edge. Every medial vertex is labelled by its source edge; same-level source edges show the BFS level-chain tooth layers, and interlevel source edges show the annular layers. Currently computed tire walk-depth labels and cut labels are overlaid without moving the medial vertices away from their source edges.""" G, M = result["G"], result["M"] levels = nx.single_source_shortest_path_length(G, result["source"]) medial_pos = {edge: _edge_midpoint(pos, edge) for edge in M.nodes()} apex_roles = {} apex_walks = {} for r in result["labels"]: apex_roles[r["apex"]] = r["role"] apex_walks.setdefault(r["apex"], []).append(r["walk"]) cut_records = [] cut_number = 1 for c in result.get("cap_cuts", []): cut_records.append((cut_number, c["medial_vertex"], "cap", c)) cut_number += 1 for d in sorted(result["results"]): rec = result["results"][d] g, bij = rec["g"], rec["bij"] for c in rec["cuts"]: if c.vertex is None: continue cut_records.append((cut_number, bij[f"a{c.vertex}"], d, c)) cut_number += 1 L = [] A = L.append A(f"\\begin{{tikzpicture}}[scale={scale},") A(" base/.style={black!12, line width=0.25pt},") A(" med/.style={black!38, line width=0.32pt},") A(" annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},") A(" levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},") A(" levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},") A(" levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},") A(" knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},") A(" elbl/.style={font=\\tiny, text=black!70, inner sep=0.2pt},") A(" dlbl/.style={font=\\tiny\\bfseries, text=black, inner sep=0.5pt},") A(" cut/.style={red!80!black, line width=1.0pt},") A(" cutlbl/.style={font=\\tiny, text=red!75!black}]") def pt_med(edge): x, y = medial_pos[edge] return f"({x:.3f},{y:.3f})" def pt_src(v): x, y = pos[v] return f"({x:.3f},{y:.3f})" for u, v in sorted(result["G"].edges()): A(f"\\draw[base] {pt_src(u)}--{pt_src(v)};") for u, v in M.edges(): A(f"\\draw[med] {pt_med(u)}--{pt_med(v)};") def chain_style(edge): u, v = edge lu, lv = levels[u], levels[v] if lu != lv: return "annv" if edge in apex_roles: return "knownv" return {1: "levone", 2: "levtwo", 3: "levthree"}.get(lu, "annv") for mv in sorted(M.nodes()): A(f"\\node[{chain_style(mv)}] at {pt_med(mv)} {{}};") for mv in sorted(M.nodes()): x, y = medial_pos[mv] A(f"\\node[elbl] at ({x:.3f},{y:.3f}) [yshift=-4.8pt] {{{_edge_label(mv)}}};") for mv in sorted(apex_walks): x, y = medial_pos[mv] label = ",".join(str(w) for w in sorted(apex_walks[mv])) A(f"\\node[dlbl] at ({x:.3f},{y:.3f}) [yshift=5.0pt] {{{label}}};") for number, mv, _d, _cut in cut_records: u, v = mv ux, uy = pos[u] vx, vy = pos[v] mx, my = medial_pos[mv] ex, ey = vx - ux, vy - uy length = math.hypot(ex, ey) or 1.0 dx, dy = -0.035 * ey / length, 0.035 * ex / length A(f"\\draw[cut] ({mx - dx:.3f},{my - dy:.3f})--({mx + dx:.3f},{my + dy:.3f});") A(f"\\node[cutlbl] at ({mx + 2.4 * dx:.3f},{my + 2.4 * dy:.3f}) {{cut {number}}};") A("\\end{tikzpicture}") return "\n".join(L) def medial_tikz(result: dict, scale: float = 7.0) -> str: """Two-panel TikZ for Figure 3: the source graph and the midpoint drawing of its medial graph with all medial vertices labelled, plus the tire walk-depth labels and cuts.""" pos = _source_layout(result["G"]) source = _source_graph_tikz(result, pos, scale=0.58 * scale) medial = _medial_midpoint_tikz(result, pos, scale=scale) return "\n".join([ "\\begin{tabular}{c}", source, "\\\\[-0.25ex]", "{\\scriptsize source graph $G$}", "\\\\[1.0ex]", medial, "\\\\[-0.25ex]", "{\\scriptsize medial graph $M(G)$ at edge midpoints}", "\\end{tabular}", ]) 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("--min-degree", type=int, default=5, help="reject random graphs below this minimum degree") parser.add_argument("--attempts", type=int, default=1000, help="number of consecutive seeds to try for --min-degree") 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, min_degree=args.min_degree, attempts=args.attempts) treads = sorted(result["results"]) print(f"% whole medial graph: n={args.n} seed={args.seed} " f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} " 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 7.0)) return result, panels = tikz_panels(args.n, args.seed, scale=args.scale, min_degree=args.min_degree, attempts=args.attempts) treads = sorted(result["results"]) print(f"% medial tire cut: n={args.n} seed={args.seed} " f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} " 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()