diff --git a/papers/medial_tire_cuts/experiments/draw_medial_tire_cut.py b/papers/medial_tire_cuts/experiments/draw_medial_tire_cut.py index 38e20ad..472ccf7 100644 --- a/papers/medial_tire_cuts/experiments/draw_medial_tire_cut.py +++ b/papers/medial_tire_cuts/experiments/draw_medial_tire_cut.py @@ -29,7 +29,10 @@ 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 diff --git a/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.md b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.md new file mode 100644 index 0000000..0a38fbf --- /dev/null +++ b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.md @@ -0,0 +1,36 @@ +# Full medial tire cut walk 1 + +- base vertices: 20 +- deep-embedded vertices: 30 +- deep-embedded edges: 84 +- graph seed: 59 +- deep-embedded minimum degree: 3 +- chosen source cap vertex: 24 +- recognised treads: 11 +- skipped treads: [(0, 'only 0 up teeth')] +- removed source-dual edges: 29 +- annular/cap cuts: 12 +- up-apex cuts: 17 + +- dual cut figure: `full_medial_tire_cut_walk_1_dual.png` +- tire cut grid: `full_medial_tire_cut_walk_1_tires.png` +- combined PDF: `full_medial_tire_cut_walk_1.pdf` + +| tread | depth | component | annular | up | singleton down | bite apexes | entry | closing cuts | up-apex cuts | shared/entry skipped | +|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:| +| T1 | 1 | 0 | 9 | 3 | 6 | 0 | e2 | 1 | 2 | 1 | +| T2 | 2 | 0 | 17 | 6 | 11 | 0 | e15 | 1 | 5 | 1 | +| T3 | 3 | 0 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 | +| T4 | 3 | 1 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 | +| T5 | 3 | 2 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 | +| T6 | 3 | 3 | 3 | 3 | 0 | 0 | e0 | 1 | 2 | 1 | +| T7 | 3 | 4 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 | +| T8 | 3 | 5 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 | +| T9 | 3 | 6 | 3 | 3 | 0 | 0 | e0 | 1 | 2 | 1 | +| T10 | 3 | 7 | 3 | 3 | 0 | 0 | e2 | 1 | 2 | 1 | +| T11 | 3 | 8 | 3 | 3 | 0 | 0 | e2 | 1 | 2 | 1 | + +## Removed Source-Dual Edges + +- annular/cap: `[(0, 20), (0, 21), (1, 6), (7, 8), (11, 25), (11, 26), (12, 27), (15, 29), (16, 28), (19, 24), (22, 5), (23, 4)]` +- up apexes: `[(0, 5), (1, 5), (2, 3), (2, 7), (4, 5), (8, 9), (10, 3), (10, 18), (11, 16), (12, 15), (12, 16), (13, 14), (13, 15), (14, 4), (16, 17), (18, 6), (19, 9)]` 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 new file mode 100644 index 0000000..7a3510f Binary files /dev/null and b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1.pdf differ diff --git a/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_dual.png b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_dual.png new file mode 100644 index 0000000..6de6864 Binary files /dev/null and b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_dual.png differ diff --git a/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_tires.png b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_tires.png new file mode 100644 index 0000000..3cc94be Binary files /dev/null and b/papers/medial_tire_cuts/experiments/full_walk/full_medial_tire_cut_walk_1_tires.png differ diff --git a/papers/medial_tire_cuts/experiments/medial_tire_cut_labelling.py b/papers/medial_tire_cuts/experiments/medial_tire_cut_labelling.py index 530c32b..9d2a40d 100644 --- a/papers/medial_tire_cuts/experiments/medial_tire_cut_labelling.py +++ b/papers/medial_tire_cuts/experiments/medial_tire_cut_labelling.py @@ -1,387 +1,16 @@ -"""Walk-depth labelling and cut of a full medial tire graph. - -Implements the procedure of Definition 2.1 ("Walk-depth labelling and cut") of -the *Medial Tire Cuts* paper: - - 1. Pick an arbitrary up tooth, the entry tooth; it has walk depth d. - 2. Traverse all teeth bounding the inner face incident to the entry tooth - clockwise until reaching the entry tooth, incrementing the walk depth by 1 - for each tooth traversed. - 3. On reaching the last tooth in the face, perform a cut by duplicating the - annular vertex at which the traversal closes (the annular vertex shared by - the last tooth and the closing tooth). - 4. Find the tooth t of highest walk depth that is a member of a bite. - 5. If t is incident to a face F with unlabelled teeth, traverse the teeth of F - starting from t in the direction of the unlabelled tooth incident to t - (sharing an annular vertex), incrementing the walk depth as you go. - 6. Repeat steps 3-5 until all teeth are labelled. - -The full medial tire graph model (annular cycle A(T), up/down teeth, bites, the -auxiliary plane graph B(T) and its inner faces) is the one from the companion -``full_medial_tire_generator.py`` of the medial tire decompositions paper, which -we import. - -Teeth are identified with the annular edges that carry them: edge i sits on the -annular vertices a_i and a_{(i+1) mod n} and carries exactly one tooth. A bite -(i, j) carries two teeth, one on edge i and one on edge j, that share the bite -apex p. The inner non-tooth faces of B(T) are the root face (written ``None``) -and one inner-gap face per bite. -""" +"""Compatibility wrapper for the medial tire cut labelling script.""" from __future__ import annotations -import argparse -import math import os import sys -# Import the full medial tire model from the companion paper's experiments. -_GEN_DIR = os.path.normpath(os.path.join( - os.path.dirname(__file__), "..", "..", - "medial_tire_decompositions_of_plane_triangulations", "experiments", -)) -sys.path.insert(0, _GEN_DIR) +PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LIB_DIR = os.path.join(PAPER_DIR, "lib") +if LIB_DIR not in sys.path: + sys.path.insert(0, LIB_DIR) -from full_medial_tire_generator import ( # noqa: E402 - FullMedialTireGraph, - has_incident_bite, - innermost_bite, - satisfies_bite_face_condition, -) - -Face = "tuple[int, int] | None" # a bite (i, j), or None for the root face - - -# --------------------------------------------------------------------------- -# Face structure of B(T). -# --------------------------------------------------------------------------- - -def parent_face(graph: FullMedialTireGraph, bite: tuple[int, int]) -> Face: - """The face directly enclosing ``bite``: the minimal-span bite strictly - containing it, or the root face ``None``.""" - i, j = bite - enclosing = [b for b in graph.bites if b[0] < i and b[1] > j] - if not enclosing: - return None - return min(enclosing, key=lambda b: b[1] - b[0]) - - -def door_bite(graph: FullMedialTireGraph, edge: int) -> tuple[int, int] | None: - """The bite that ``edge`` is a door of (i.e. a bite edge), or None.""" - for b in graph.bites: - if edge in b: - return b - return None - - -def faces_bordered(graph: FullMedialTireGraph, edge: int) -> list[Face]: - """The inner non-tooth faces whose boundary the tooth on ``edge`` lies on. - - A bite door borders two faces (its bite's gap and that bite's parent); any - other tooth borders the single face directly containing its edge. - """ - bite = door_bite(graph, edge) - if bite is not None: - return [bite, parent_face(graph, bite)] - return [innermost_bite(edge, graph.bites)] - - -def face_boundary(graph: FullMedialTireGraph, face: Face) -> list[int]: - """The teeth (annular edges) bounding ``face``, in clockwise cyclic order. - - Clockwise is increasing edge index. For the root face the boundary is read - around the whole cycle; for a bite gap (i, j) it is read along the arc - i, i+1, ..., j and closes through the bite apex. Edges enclosed by a child - bite are skipped (they belong to the child's gap face). - """ - n = graph.n - arc = range(n) if face is None else range(face[0], face[1] + 1) - return [k for k in arc if face in faces_bordered(graph, k)] - - -def all_faces(graph: FullMedialTireGraph) -> list[Face]: - return [None] + sorted(graph.bites) - - -def shared_annular_vertex(graph: FullMedialTireGraph, e1: int, e2: int) -> int | None: - """The annular vertex a_k shared by edges ``e1`` and ``e2``, or None.""" - n = graph.n - common = {e1, (e1 + 1) % n} & {e2, (e2 + 1) % n} - return next(iter(common)) if common else None - - -# --------------------------------------------------------------------------- -# The walk-depth labelling and cut. -# --------------------------------------------------------------------------- - -class Cut: - """A cut performed when a face traversal closes: the duplicated annular - vertex, together with the last labelled tooth and the closing tooth that - share it, and the face being closed.""" - - __slots__ = ("vertex", "last_tooth", "closing_tooth", "face", "order") - - def __init__(self, vertex, last_tooth, closing_tooth, face, order): - self.vertex = vertex - self.last_tooth = last_tooth - self.closing_tooth = closing_tooth - self.face = face - self.order = order - - def __repr__(self): - f = "root" if self.face is None else f"bite{self.face}" - return (f"Cut(order={self.order}, a{self.vertex}, " - f"last=e{self.last_tooth}, closing=e{self.closing_tooth}, face={f})") - - -def label_and_cut(graph: FullMedialTireGraph, entry_edge: int, - start_depth: int = 0) -> tuple[dict[int, int], list[Cut]]: - """Run the procedure starting from up tooth ``entry_edge``. - - Returns ``(depth, cuts)`` where ``depth`` maps each annular edge (tooth) to - its walk depth, and ``cuts`` is the list of cuts in the order performed. - """ - if graph.tooth_word[entry_edge] != "U": - raise ValueError(f"entry edge {entry_edge} is not an up tooth") - - depth: dict[int, int] = {} - cuts: list[Cut] = [] - counter = start_depth - - def traverse(face: Face, start_edge: int, is_entry: bool) -> None: - nonlocal counter - boundary = face_boundary(graph, face) - m = len(boundary) - pos = boundary.index(start_edge) - if is_entry: - depth[start_edge] = counter - counter += 1 - direction = +1 - else: - # head toward the unlabelled tooth incident to the door t - direction = +1 if boundary[(pos + 1) % m] not in depth else -1 - last_new = start_edge - i = pos - while True: - i = (i + direction) % m - edge = boundary[i] - if edge in depth: # the closing tooth - cuts.append(Cut( - vertex=shared_annular_vertex(graph, last_new, edge), - last_tooth=last_new, closing_tooth=edge, - face=face, order=len(cuts), - )) - return - depth[edge] = counter - counter += 1 - last_new = edge - - # Steps 1-3: the entry face. - traverse(innermost_bite(entry_edge, graph.bites), entry_edge, is_entry=True) - - # Steps 4-6: descend (or ascend) through bites, deepest first. The root - # face is ``None``, so we use a distinct sentinel for "no unlabelled face". - _MISSING = object() - while len(depth) < graph.n: - labelled_bite_teeth = sorted( - (e for e in depth if door_bite(graph, e) is not None), - key=lambda e: depth[e], reverse=True, - ) - for t in labelled_bite_teeth: - target = next((F for F in faces_bordered(graph, t) - if any(e not in depth for e in face_boundary(graph, F))), - _MISSING) - if target is not _MISSING: - traverse(target, t, is_entry=False) - break - else: - break # no progress possible - - return depth, cuts - - -# --------------------------------------------------------------------------- -# TikZ rendering. -# --------------------------------------------------------------------------- - -def _coords(graph: FullMedialTireGraph, - r_ann=1.0, r_up=1.46, r_down=0.60) -> dict[str, tuple[float, float]]: - n = graph.n - - def ang(k): # a_0 at the top, increasing k clockwise - return math.radians(90.0 - k * 360.0 / n) - - def edge_mid_dir(i): # angle of the bisector of edge i's two endpoints - a0, a1 = ang(i), ang((i + 1) % n) - return math.atan2(math.sin(a0) + math.sin(a1), math.cos(a0) + math.cos(a1)) - - pos = {f"a{k}": (r_ann * math.cos(ang(k)), r_ann * math.sin(ang(k))) - for k in range(n)} - for i in graph.up_edges: - a = edge_mid_dir(i) - pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a)) - for i in graph.singleton_down_edges: - a = edge_mid_dir(i) - pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a)) - for (i, j) in graph.bites: - pts = [pos[f"a{i}"], pos[f"a{(i + 1) % n}"], - pos[f"a{j}"], pos[f"a{(j + 1) % n}"]] - cx = sum(p[0] for p in pts) / 4.0 - cy = sum(p[1] for p in pts) / 4.0 - pos[f"p{i}_{j}"] = (0.9 * cx, 0.9 * cy) - return pos - - -def _edge_midpoint(pos, graph, edge): - n = graph.n - a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"] - return (0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1])) - - -def to_tikz(graph: FullMedialTireGraph, - depth: dict[int, int] | None = None, - cuts: list[Cut] | None = None, - entry_edge: int | None = None, - scale: float = 2.2) -> str: - """A standalone ``tikzpicture`` for ``graph``; if ``depth`` is given, draw - the walk-depth labels and (with ``cuts``) the cut marks.""" - pos = _coords(graph) - n = graph.n - L = [] - A = L.append - A(f"\\begin{{tikzpicture}}[scale={scale},") - A(" ann/.style={circle, fill=black, inner sep=1.0pt},") - A(" upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},") - A(" downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},") - A(" bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},") - A(" cyc/.style={black, line width=1.0pt},") - A(" tth/.style={black!55, line width=0.4pt},") - A(" lbl/.style={font=\\scriptsize},") - A(" dlbl/.style={font=\\scriptsize\\bfseries, text=black},") - A(" cut/.style={red!80!black, line width=1.3pt},") - A(" cutlbl/.style={font=\\tiny, text=red!75!black}]") - - def pt(name): - x, y = pos[name] - return f"({x:.3f},{y:.3f})" - - # annular cycle - cyc = "--".join(pt(f"a{k}") for k in range(n)) + "--cycle" - A(f"\\draw[cyc] {cyc};") - # spokes - for i in graph.up_edges: - A(f"\\draw[tth] {pt(f'u{i}')}--{pt(f'a{i}')} {pt(f'u{i}')}--{pt(f'a{(i+1)%n}')};") - for i in graph.singleton_down_edges: - A(f"\\draw[tth] {pt(f'd{i}')}--{pt(f'a{i}')} {pt(f'd{i}')}--{pt(f'a{(i+1)%n}')};") - for (i, j) in graph.bites: - apex = f"p{i}_{j}" - for e in (i, j): - A(f"\\draw[tth] {pt(apex)}--{pt(f'a{e}')} {pt(apex)}--{pt(f'a{(e+1)%n}')};") - # vertices - for k in range(n): - A(f"\\node[ann] at {pt(f'a{k}')} {{}};") - for i in graph.up_edges: - A(f"\\node[upv] at {pt(f'u{i}')} {{}};") - for i in graph.singleton_down_edges: - A(f"\\node[downv] at {pt(f'd{i}')} {{}};") - for (i, j) in sorted(graph.bites): - A(f"\\node[bitev] at {pt(f'p{i}_{j}')} {{}};") - - # walk-depth labels: placed along the spoke from apex toward the edge mid - if depth is not None: - for edge in range(n): - apex = graph.apex_of_edge(edge) - ax, ay = pos[apex] - mx, my = _edge_midpoint(pos, graph, edge) - f = 0.5 - lx, ly = ax + f * (mx - ax), ay + f * (my - ay) - A(f"\\node[dlbl] at ({lx:.3f},{ly:.3f}) {{{depth[edge]}}};") - - # cut marks: a short red slit across the duplicated annular vertex - if cuts: - for c in cuts: - if c.vertex is None: - continue - vx, vy = pos[f"a{c.vertex}"] - rad = math.atan2(vy, vx) - dx, dy = 0.16 * math.cos(rad), 0.16 * math.sin(rad) - A(f"\\draw[cut] ({vx-dx:.3f},{vy-dy:.3f})--({vx+dx:.3f},{vy+dy:.3f});") - lx, ly = vx + 0.30 * math.cos(rad), vy + 0.30 * math.sin(rad) - A(f"\\node[cutlbl] at ({lx:.3f},{ly:.3f}) {{cut {c.order+1}}};") - - if entry_edge is not None: - ex, ey = pos[graph.apex_of_edge(entry_edge)] - rad = math.atan2(ey, ex) - tx, ty = ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad) - A(f"\\node[lbl, text=blue!60!black] at ({tx:.3f},{ty:.3f}) {{entry}};") - - A("\\end{tikzpicture}") - return "\n".join(L) - - -# --------------------------------------------------------------------------- -# Worked example and CLI. -# --------------------------------------------------------------------------- - -def worked_example() -> FullMedialTireGraph: - """A clean 8-tooth piece: one bite (0,4), three down singletons 1,2,3 in its - gap, three up teeth 5,6,7 in the root face.""" - return FullMedialTireGraph(n=8, tooth_word="DDDDDUUU", bites=frozenset({(0, 4)})) - - -def _check(graph: FullMedialTireGraph) -> None: - assert not has_incident_bite(graph.bites, graph.n), "bite uses incident edges" - assert satisfies_bite_face_condition(graph.tooth_word, graph.bites), \ - "violates the bite-face condition" - assert graph.tooth_word.count("U") >= 3, "fewer than three up teeth" - - -def _describe(graph, depth, cuts) -> str: - lines = ["edge type walk-depth"] - for e in range(graph.n): - t = graph.tooth_word[e] - kind = {"U": "up"}.get(t, "down") - if door_bite(graph, e) is not None: - kind = "bite" - lines.append(f" e{e} {kind:<5} {depth[e]}") - lines.append("cuts (in order):") - for c in cuts: - f = "root" if c.face is None else f"bite{c.face}" - lines.append(f" cut {c.order+1}: duplicate a{c.vertex} " - f"(closing tooth e{c.closing_tooth} of {f})") - return "\n".join(lines) - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--entry", default="u5", - help="entry up tooth, as an edge index or apex name like u5") - parser.add_argument("--start-depth", type=int, default=0) - parser.add_argument("--tikz", choices=["plain", "labelled", "both"], - help="emit TikZ for the worked example") - args = parser.parse_args() - - entry = args.entry - edge = int(entry[1:]) if isinstance(entry, str) and entry.startswith("u") else int(entry) - - graph = worked_example() - _check(graph) - depth, cuts = label_and_cut(graph, edge, start_depth=args.start_depth) - - if args.tikz == "plain": - print(to_tikz(graph)) - elif args.tikz == "labelled": - print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge)) - elif args.tikz == "both": - print("% --- plain ---") - print(to_tikz(graph)) - print("% --- labelled + cut ---") - print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge)) - else: - print(f"worked example: n={graph.n} word={graph.tooth_word} " - f"bites={sorted(graph.bites)} entry=e{edge}") - print(_describe(graph, depth, cuts)) +from medial_tire_cut_labelling import main if __name__ == "__main__": 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 a0d9d2d..6925402 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 @@ -50,8 +50,11 @@ _HERE = os.path.dirname(os.path.abspath(__file__)) _MTD = os.path.normpath(os.path.join( _HERE, "..", "..", "medial_tire_decompositions_of_plane_triangulations", "experiments")) -sys.path.insert(0, _MTD) +_PAPER_DIR = os.path.dirname(_HERE) +_CUT_LIB = os.path.join(_PAPER_DIR, "lib") sys.path.insert(0, _HERE) +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, @@ -61,6 +64,7 @@ from run_medial_tire_cut_experiment import ( # noqa: E402 _assemble_cut_graph, _cap_cut, _label_treads, random_maximal_planar_min_degree, ) +from medial_tire_cut_labelling import up_apex_cuts # noqa: E402 # --------------------------------------------------------------------------- # @@ -204,16 +208,13 @@ 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 (singleton) up tooth across all treads, except the - entry tooth of each tread (its apex is not duplicated).""" + 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.""" removed = set() for key in sorted(results): g, bij = results[key]["g"], results[key]["bij"] entry = results[key]["entry_edge"] - for i in g.up_edges: - if i == entry: - continue - removed.add(bij[f"u{i}"]) + removed.update(up_apex_cuts(g, entry, bij=bij).values()) return removed @@ -412,16 +413,8 @@ def _radial_source_layout(G, source, levels): return pos -def draw_png(result, path, scale=6.0): - """Render the source-dual cut: dual nodes at face centroids, dual edges - drawn light gray where the cut removed them, labelled by missing count. - - The source graph is laid out concentrically around the cap source so the - BFS/plane-depth rings read as nested circles.""" - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - +def _draw_dual_cut_ax(ax, result): + """Draw the source-dual cut on an existing Matplotlib axis.""" G, faces, dual = result["G"], result["faces"], result["dual"] removed = result["removed_dual_edges"] missing = result["dual_face_missing"] @@ -435,7 +428,6 @@ def draw_png(result, path, scale=6.0): return (sum(xs) / 3.0, sum(ys) / 3.0) pos = {fi: centroid(fi) for fi in dual.nodes()} - fig, ax = plt.subplots(figsize=(7.6, 7.6)) # primal (source) graph, faint, for orientation for u, v in G.edges(): ax.plot([pos_v[u][0], pos_v[v][0]], [pos_v[u][1], pos_v[v][1]], @@ -491,6 +483,20 @@ def draw_png(result, path, scale=6.0): f"max {result['max_missing']}", fontsize=9) ax.set_aspect("equal") ax.axis("off") + + +def draw_png(result, path, scale=6.0): + """Render the source-dual cut: dual nodes at face centroids, dual edges + drawn light gray where the cut removed them, labelled by missing count. + + The source graph is laid out concentrically around the cap source so the + BFS/plane-depth rings read as nested circles.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(7.6, 7.6)) + _draw_dual_cut_ax(ax, result) fig.tight_layout() fig.savefig(path, dpi=150) plt.close(fig) @@ -583,10 +589,8 @@ def _draw_tread(ax, g, depth, cuts, entry_edge, title): f"cut {c.order + 1}", fontsize=6, color="#cc2020", ha="center", va="center", zorder=5) # up-tooth apex duplications (slit tangential, across the apex marker); - # the entry tooth's apex is not duplicated - for i in g.up_edges: - if i == entry_edge: - continue + # entry and shared up-apex vertices are not duplicated. + for i in up_apex_cuts(g, entry_edge): vx, vy = pos[f"u{i}"] rad = math.atan2(vy, vx) tx, ty = -math.sin(rad), math.cos(rad) # tangential @@ -635,6 +639,116 @@ def draw_tire_cuts_png(result, path): plt.close(fig) +def _tire_tree_edges(result): + """Parent-child edges between recognised tires, using chained entry apexes.""" + res = result["results"] + edges = [] + for child in sorted(res): + d, _comp = child + if d == min(k[0] for k in res): + continue + cg, cbij = res[child]["g"], res[child]["bij"] + entry = res[child]["entry_edge"] + child_apex = cbij[f"u{entry}"] + for parent in sorted(k for k in res if k[0] == d - 1): + pg, pbij = res[parent]["g"], res[parent]["bij"] + if any(pbij[pg.apex_of_edge(e)] == child_apex for e in pg.down_edges): + edges.append((parent, child)) + break + return edges + + +def _draw_tire_tree_ax(ax, result): + """Compact tire-tree panel, styled like the decomposition overview.""" + res = result["results"] + keys = sorted(res) + if not keys: + ax.text(0.5, 0.5, "no recognised tires", ha="center", va="center") + ax.axis("off") + return + by_depth = defaultdict(list) + for key in keys: + by_depth[key[0]].append(key) + pos = {} + depths = sorted(by_depth) + for row, d in enumerate(depths): + group = by_depth[d] + for i, key in enumerate(group): + x = i - (len(group) - 1) / 2 + y = -1.25 * row + pos[key] = (x, y) + for parent, child in _tire_tree_edges(result): + x1, y1 = pos[parent] + x2, y2 = pos[child] + ax.plot([x1, x2], [y1, y2], color="0.45", lw=1.0, zorder=1) + for idx, key in enumerate(keys, start=1): + d, comp = key + rec = res[key] + g = rec["g"] + x, y = pos[key] + ax.scatter([x], [y], s=300, marker="s", facecolor="#eef3fa", + edgecolor="#3a6ea5", linewidth=1.0, zorder=2) + ax.text( + x, y, + f"T{idx}\n{d}.{comp}\n|A|={g.n}", + ha="center", va="center", fontsize=6, zorder=3, + ) + ax.set_title("tire tree", fontsize=10) + ax.margins(x=0.08, y=0.18) + ax.axis("off") + + +def draw_combined_pdf(result, path): + """Draw the source-dual cut, tire tree, and all tire panels in one PDF.""" + import math + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + keys = sorted(result["results"]) + if not keys: + raise ValueError("no recognised tires to draw") + + fig = plt.figure(figsize=(17, 10)) + spec = fig.add_gridspec(2, 2, width_ratios=[1.05, 1.35], + height_ratios=[1.1, 0.9]) + ax_dual = fig.add_subplot(spec[0, 0]) + ax_tree = fig.add_subplot(spec[1, 0]) + _draw_dual_cut_ax(ax_dual, result) + _draw_tire_tree_ax(ax_tree, result) + + cols = min(4, len(keys)) + rows = math.ceil(len(keys) / cols) + tire_spec = spec[:, 1].subgridspec(rows, cols, wspace=0.18, hspace=0.28) + for i, key in enumerate(keys): + ax = fig.add_subplot(tire_spec[i // cols, i % cols]) + d, comp = key + rec = result["results"][key] + g = rec["g"] + title = ( + f"T{i + 1} d={d}.{comp} |A|={g.n} {g.tooth_word}\n" + f"entry=e{rec['entry_edge']} start={rec['start_depth']} " + f"closing={len(rec['cuts'])} apex={len(up_apex_cuts(g, rec['entry_edge'], rec['bij']))}" + ) + _draw_tread(ax, g, rec["depth"], rec["cuts"], rec["entry_edge"], title) + for i in range(len(keys), rows * cols): + ax = fig.add_subplot(tire_spec[i // cols, i % cols]) + ax.axis("off") + + base = result.get("base_graph") + fig.suptitle( + "Full medial tire cut walk: " + f"base n={base.number_of_nodes() if base is not None else '?'}; " + f"deep n={result['G'].number_of_nodes()}; source={result['source']}; " + f"root entry=e{result['entry_edge']}; removed dual edges={len(result['removed_dual_edges'])}", + fontsize=13, + ) + fig.subplots_adjust(left=0.03, right=0.99, top=0.90, bottom=0.04, + wspace=0.10, hspace=0.16) + fig.savefig(path) + plt.close(fig) + + def draw_cap_png(result, path): """Render tread 0, the source cap: a wheel with the source at the hub, its link cycle as the rim, the cap triangles (down teeth) filled, and the cap @@ -726,6 +840,8 @@ def main(): help="render each full medial tire cut to PNG") parser.add_argument("--cap-png", metavar="PATH", help="render tread 0 (the source cap) to PNG") + parser.add_argument("--pdf", metavar="PATH", + help="render dual, tire tree, and tire cuts in one PDF") args = parser.parse_args() rng = random.Random(args.seed) @@ -759,6 +875,9 @@ def main(): if args.cap_png: draw_cap_png(result, args.cap_png) print(f"wrote {args.cap_png}") + if args.pdf: + draw_combined_pdf(result, args.pdf) + print(f"wrote {args.pdf}") if __name__ == "__main__": 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 e25f29a..6ccbb71 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 @@ -49,8 +49,11 @@ _HERE = os.path.dirname(os.path.abspath(__file__)) _MTD = os.path.normpath(os.path.join( _HERE, "..", "..", "medial_tire_decompositions_of_plane_triangulations", "experiments")) -sys.path.insert(0, _MTD) +_PAPER_DIR = os.path.dirname(_HERE) +_CUT_LIB = os.path.join(_PAPER_DIR, "lib") sys.path.insert(0, _HERE) +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, diff --git a/papers/medial_tire_cuts/lib/__init__.py b/papers/medial_tire_cuts/lib/__init__.py new file mode 100644 index 0000000..094a291 --- /dev/null +++ b/papers/medial_tire_cuts/lib/__init__.py @@ -0,0 +1 @@ +"""Reusable medial tire cut helpers.""" diff --git a/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py b/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py new file mode 100644 index 0000000..83ffb33 --- /dev/null +++ b/papers/medial_tire_cuts/lib/medial_tire_cut_labelling.py @@ -0,0 +1,429 @@ +"""Walk-depth labelling and cut of a full medial tire graph. + +Implements the procedure of Definition 2.1 ("Walk-depth labelling and cut") of +the *Medial Tire Cuts* paper: + + 1. Pick an arbitrary up tooth, the entry tooth; it has walk depth d. + 2. Traverse all teeth bounding the inner face incident to the entry tooth + clockwise until reaching the entry tooth, incrementing the walk depth by 1 + for each tooth traversed. + 3. On reaching the last tooth in the face, perform a cut by duplicating the + annular vertex at which the traversal closes (the annular vertex shared by + the last tooth and the closing tooth). + 4. Find the tooth t of highest walk depth that is a member of a bite. + 5. If t is incident to a face F with unlabelled teeth, traverse the teeth of F + starting from t in the direction of the unlabelled tooth incident to t + (sharing an annular vertex), incrementing the walk depth as you go. + 6. Repeat steps 3-5 until all teeth are labelled. + 7. Cut the apex of every up tooth, except entry teeth and except any apex + vertex that is shared by two up teeth. + +The full medial tire graph model (annular cycle A(T), up/down teeth, bites, the +auxiliary plane graph B(T) and its inner faces) is the one from the companion +``full_medial_tire_generator.py`` of the medial tire decompositions paper, which +we import. + +Teeth are identified with the annular edges that carry them: edge i sits on the +annular vertices a_i and a_{(i+1) mod n} and carries exactly one tooth. A bite +(i, j) carries two teeth, one on edge i and one on edge j, that share the bite +apex p. The inner non-tooth faces of B(T) are the root face (written ``None``) +and one inner-gap face per bite. +""" + +from __future__ import annotations + +import argparse +import math +import os +import sys +from collections import Counter +from collections.abc import Mapping + +# Import the full medial tire model from the companion paper's lib directory. +_GEN_DIR = os.path.normpath(os.path.join( + os.path.dirname(__file__), "..", "..", + "medial_tire_decompositions_of_plane_triangulations", "lib", +)) +sys.path.insert(0, _GEN_DIR) + +from full_medial_tire_generator import ( # noqa: E402 + FullMedialTireGraph, + has_incident_bite, + innermost_bite, + satisfies_bite_face_condition, +) + +Face = "tuple[int, int] | None" # a bite (i, j), or None for the root face + + +# --------------------------------------------------------------------------- +# Face structure of B(T). +# --------------------------------------------------------------------------- + +def parent_face(graph: FullMedialTireGraph, bite: tuple[int, int]) -> Face: + """The face directly enclosing ``bite``: the minimal-span bite strictly + containing it, or the root face ``None``.""" + i, j = bite + enclosing = [b for b in graph.bites if b[0] < i and b[1] > j] + if not enclosing: + return None + return min(enclosing, key=lambda b: b[1] - b[0]) + + +def door_bite(graph: FullMedialTireGraph, edge: int) -> tuple[int, int] | None: + """The bite that ``edge`` is a door of (i.e. a bite edge), or None.""" + for b in graph.bites: + if edge in b: + return b + return None + + +def faces_bordered(graph: FullMedialTireGraph, edge: int) -> list[Face]: + """The inner non-tooth faces whose boundary the tooth on ``edge`` lies on. + + A bite door borders two faces (its bite's gap and that bite's parent); any + other tooth borders the single face directly containing its edge. + """ + bite = door_bite(graph, edge) + if bite is not None: + return [bite, parent_face(graph, bite)] + return [innermost_bite(edge, graph.bites)] + + +def face_boundary(graph: FullMedialTireGraph, face: Face) -> list[int]: + """The teeth (annular edges) bounding ``face``, in clockwise cyclic order. + + Clockwise is increasing edge index. For the root face the boundary is read + around the whole cycle; for a bite gap (i, j) it is read along the arc + i, i+1, ..., j and closes through the bite apex. Edges enclosed by a child + bite are skipped (they belong to the child's gap face). + """ + n = graph.n + arc = range(n) if face is None else range(face[0], face[1] + 1) + return [k for k in arc if face in faces_bordered(graph, k)] + + +def all_faces(graph: FullMedialTireGraph) -> list[Face]: + return [None] + sorted(graph.bites) + + +def shared_annular_vertex(graph: FullMedialTireGraph, e1: int, e2: int) -> int | None: + """The annular vertex a_k shared by edges ``e1`` and ``e2``, or None.""" + n = graph.n + common = {e1, (e1 + 1) % n} & {e2, (e2 + 1) % n} + return next(iter(common)) if common else None + + +# --------------------------------------------------------------------------- +# The walk-depth labelling and cut. +# --------------------------------------------------------------------------- + +class Cut: + """A cut performed when a face traversal closes: the duplicated annular + vertex, together with the last labelled tooth and the closing tooth that + share it, and the face being closed.""" + + __slots__ = ("vertex", "last_tooth", "closing_tooth", "face", "order") + + def __init__(self, vertex, last_tooth, closing_tooth, face, order): + self.vertex = vertex + self.last_tooth = last_tooth + self.closing_tooth = closing_tooth + self.face = face + self.order = order + + def __repr__(self): + f = "root" if self.face is None else f"bite{self.face}" + return (f"Cut(order={self.order}, a{self.vertex}, " + f"last=e{self.last_tooth}, closing=e{self.closing_tooth}, face={f})") + + +def label_and_cut(graph: FullMedialTireGraph, entry_edge: int, + start_depth: int = 0) -> tuple[dict[int, int], list[Cut]]: + """Run the procedure starting from up tooth ``entry_edge``. + + Returns ``(depth, cuts)`` where ``depth`` maps each annular edge (tooth) to + its walk depth, and ``cuts`` is the list of cuts in the order performed. + """ + if graph.tooth_word[entry_edge] != "U": + raise ValueError(f"entry edge {entry_edge} is not an up tooth") + + depth: dict[int, int] = {} + cuts: list[Cut] = [] + counter = start_depth + + def traverse(face: Face, start_edge: int, is_entry: bool) -> None: + nonlocal counter + boundary = face_boundary(graph, face) + m = len(boundary) + pos = boundary.index(start_edge) + if is_entry: + depth[start_edge] = counter + counter += 1 + direction = +1 + else: + # head toward the unlabelled tooth incident to the door t + direction = +1 if boundary[(pos + 1) % m] not in depth else -1 + last_new = start_edge + i = pos + while True: + i = (i + direction) % m + edge = boundary[i] + if edge in depth: # the closing tooth + cuts.append(Cut( + vertex=shared_annular_vertex(graph, last_new, edge), + last_tooth=last_new, closing_tooth=edge, + face=face, order=len(cuts), + )) + return + depth[edge] = counter + counter += 1 + last_new = edge + + # Steps 1-3: the entry face. + traverse(innermost_bite(entry_edge, graph.bites), entry_edge, is_entry=True) + + # Steps 4-6: descend (or ascend) through bites, deepest first. The root + # face is ``None``, so we use a distinct sentinel for "no unlabelled face". + _MISSING = object() + while len(depth) < graph.n: + labelled_bite_teeth = sorted( + (e for e in depth if door_bite(graph, e) is not None), + key=lambda e: depth[e], reverse=True, + ) + for t in labelled_bite_teeth: + target = next((F for F in faces_bordered(graph, t) + if any(e not in depth for e in face_boundary(graph, F))), + _MISSING) + if target is not _MISSING: + traverse(target, t, is_entry=False) + break + else: + break # no progress possible + + return depth, cuts + + +def up_apex_cuts(graph: FullMedialTireGraph, entry_edge: int, + bij: Mapping[str, 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 + duplicate. Entry teeth are not cut. If ``bij`` is supplied, it maps the + model vertex names (``u{i}``) into the ambient medial graph; this lets a + real tread suppress cuts at a vertex that is the shared apex of two up + teeth. Without ``bij`` the model vertex names are used directly. + """ + apex_by_edge = { + i: (bij[f"u{i}"] if bij is not None else graph.apex_of_edge(i)) + for i in graph.up_edges + } + 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 + } + + +# --------------------------------------------------------------------------- +# TikZ rendering. +# --------------------------------------------------------------------------- + +def _coords(graph: FullMedialTireGraph, + r_ann=1.0, r_up=1.46, r_down=0.60) -> dict[str, tuple[float, float]]: + n = graph.n + + def ang(k): # a_0 at the top, increasing k clockwise + return math.radians(90.0 - k * 360.0 / n) + + def edge_mid_dir(i): # angle of the bisector of edge i's two endpoints + a0, a1 = ang(i), ang((i + 1) % n) + return math.atan2(math.sin(a0) + math.sin(a1), math.cos(a0) + math.cos(a1)) + + pos = {f"a{k}": (r_ann * math.cos(ang(k)), r_ann * math.sin(ang(k))) + for k in range(n)} + for i in graph.up_edges: + a = edge_mid_dir(i) + pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a)) + for i in graph.singleton_down_edges: + a = edge_mid_dir(i) + pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a)) + for (i, j) in graph.bites: + pts = [pos[f"a{i}"], pos[f"a{(i + 1) % n}"], + pos[f"a{j}"], pos[f"a{(j + 1) % n}"]] + cx = sum(p[0] for p in pts) / 4.0 + cy = sum(p[1] for p in pts) / 4.0 + pos[f"p{i}_{j}"] = (0.9 * cx, 0.9 * cy) + return pos + + +def _edge_midpoint(pos, graph, edge): + n = graph.n + a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"] + return (0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1])) + + +def to_tikz(graph: FullMedialTireGraph, + depth: dict[int, int] | None = None, + cuts: list[Cut] | None = None, + entry_edge: int | None = None, + scale: float = 2.2) -> str: + """A standalone ``tikzpicture`` for ``graph``; if ``depth`` is given, draw + the walk-depth labels and (with ``cuts``) the cut marks.""" + pos = _coords(graph) + n = graph.n + L = [] + A = L.append + A(f"\\begin{{tikzpicture}}[scale={scale},") + A(" ann/.style={circle, fill=black, inner sep=1.0pt},") + A(" upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},") + A(" downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},") + A(" bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},") + A(" cyc/.style={black, line width=1.0pt},") + A(" tth/.style={black!55, line width=0.4pt},") + A(" lbl/.style={font=\\scriptsize},") + A(" dlbl/.style={font=\\scriptsize\\bfseries, text=black},") + A(" cut/.style={red!80!black, line width=1.3pt},") + A(" cutlbl/.style={font=\\tiny, text=red!75!black}]") + + def pt(name): + x, y = pos[name] + return f"({x:.3f},{y:.3f})" + + # annular cycle + cyc = "--".join(pt(f"a{k}") for k in range(n)) + "--cycle" + A(f"\\draw[cyc] {cyc};") + # spokes + for i in graph.up_edges: + A(f"\\draw[tth] {pt(f'u{i}')}--{pt(f'a{i}')} {pt(f'u{i}')}--{pt(f'a{(i+1)%n}')};") + for i in graph.singleton_down_edges: + A(f"\\draw[tth] {pt(f'd{i}')}--{pt(f'a{i}')} {pt(f'd{i}')}--{pt(f'a{(i+1)%n}')};") + for (i, j) in graph.bites: + apex = f"p{i}_{j}" + for e in (i, j): + A(f"\\draw[tth] {pt(apex)}--{pt(f'a{e}')} {pt(apex)}--{pt(f'a{(e+1)%n}')};") + # vertices + for k in range(n): + A(f"\\node[ann] at {pt(f'a{k}')} {{}};") + for i in graph.up_edges: + A(f"\\node[upv] at {pt(f'u{i}')} {{}};") + for i in graph.singleton_down_edges: + A(f"\\node[downv] at {pt(f'd{i}')} {{}};") + for (i, j) in sorted(graph.bites): + A(f"\\node[bitev] at {pt(f'p{i}_{j}')} {{}};") + + # walk-depth labels: placed along the spoke from apex toward the edge mid + if depth is not None: + for edge in range(n): + apex = graph.apex_of_edge(edge) + ax, ay = pos[apex] + mx, my = _edge_midpoint(pos, graph, edge) + f = 0.5 + lx, ly = ax + f * (mx - ax), ay + f * (my - ay) + A(f"\\node[dlbl] at ({lx:.3f},{ly:.3f}) {{{depth[edge]}}};") + + # cut marks: a short red slit across the duplicated annular vertex + if cuts: + for c in cuts: + if c.vertex is None: + continue + vx, vy = pos[f"a{c.vertex}"] + rad = math.atan2(vy, vx) + dx, dy = 0.16 * math.cos(rad), 0.16 * math.sin(rad) + A(f"\\draw[cut] ({vx-dx:.3f},{vy-dy:.3f})--({vx+dx:.3f},{vy+dy:.3f});") + lx, ly = vx + 0.30 * math.cos(rad), vy + 0.30 * math.sin(rad) + A(f"\\node[cutlbl] at ({lx:.3f},{ly:.3f}) {{cut {c.order+1}}};") + + # up-tooth apex cuts: tangential slits, excluding the entry tooth and any + # up apex shared by two up teeth. + if entry_edge is not None: + for i in up_apex_cuts(graph, entry_edge): + vx, vy = pos[f"u{i}"] + rad = math.atan2(vy, vx) + tx, ty = -math.sin(rad), math.cos(rad) + A(f"\\draw[cut] ({vx-0.12*tx:.3f},{vy-0.12*ty:.3f})--" + f"({vx+0.12*tx:.3f},{vy+0.12*ty:.3f});") + + if entry_edge is not None: + ex, ey = pos[graph.apex_of_edge(entry_edge)] + rad = math.atan2(ey, ex) + tx, ty = ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad) + A(f"\\node[lbl, text=blue!60!black] at ({tx:.3f},{ty:.3f}) {{entry}};") + + A("\\end{tikzpicture}") + return "\n".join(L) + + +# --------------------------------------------------------------------------- +# Worked example and CLI. +# --------------------------------------------------------------------------- + +def worked_example() -> FullMedialTireGraph: + """A clean 8-tooth piece: one bite (0,4), three down singletons 1,2,3 in its + gap, three up teeth 5,6,7 in the root face.""" + return FullMedialTireGraph(n=8, tooth_word="DDDDDUUU", bites=frozenset({(0, 4)})) + + +def _check(graph: FullMedialTireGraph) -> None: + assert not has_incident_bite(graph.bites, graph.n), "bite uses incident edges" + assert satisfies_bite_face_condition(graph.tooth_word, graph.bites), \ + "violates the bite-face condition" + assert graph.tooth_word.count("U") >= 3, "fewer than three up teeth" + + +def _describe(graph, depth, cuts, entry_edge) -> str: + lines = ["edge type walk-depth"] + for e in range(graph.n): + t = graph.tooth_word[e] + kind = {"U": "up"}.get(t, "down") + if door_bite(graph, e) is not None: + kind = "bite" + lines.append(f" e{e} {kind:<5} {depth[e]}") + lines.append("cuts (in order):") + for c in cuts: + f = "root" if c.face is None else f"bite{c.face}" + lines.append(f" cut {c.order+1}: duplicate a{c.vertex} " + f"(closing tooth e{c.closing_tooth} of {f})") + apex_cuts = up_apex_cuts(graph, entry_edge) + if apex_cuts: + lines.append("up-apex cuts:") + for edge, apex in apex_cuts.items(): + lines.append(f" duplicate {apex} for up tooth e{edge}") + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--entry", default="u5", + help="entry up tooth, as an edge index or apex name like u5") + parser.add_argument("--start-depth", type=int, default=0) + parser.add_argument("--tikz", choices=["plain", "labelled", "both"], + help="emit TikZ for the worked example") + args = parser.parse_args() + + entry = args.entry + edge = int(entry[1:]) if isinstance(entry, str) and entry.startswith("u") else int(entry) + + graph = worked_example() + _check(graph) + depth, cuts = label_and_cut(graph, edge, start_depth=args.start_depth) + + if args.tikz == "plain": + print(to_tikz(graph)) + elif args.tikz == "labelled": + print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge)) + elif args.tikz == "both": + print("% --- plain ---") + print(to_tikz(graph)) + print("% --- labelled + cut ---") + print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge)) + else: + print(f"worked example: n={graph.n} word={graph.tooth_word} " + f"bites={sorted(graph.bites)} entry=e{edge}") + print(_describe(graph, depth, cuts, edge)) + + +if __name__ == "__main__": + main() diff --git a/papers/medial_tire_cuts/paper.tex b/papers/medial_tire_cuts/paper.tex index 50106fe..9c8b457 100644 --- a/papers/medial_tire_cuts/paper.tex +++ b/papers/medial_tire_cuts/paper.tex @@ -112,9 +112,21 @@ cuts as follows. $1$ as you travel. (Here a tooth is \emph{incident to $t$} when it shares an annular vertex of $A(T)$ with $t$.) \item Repeat steps (3)--(5) until all teeth have been labelled. + \item Finally, perform an apex cut at every up tooth except an entry + tooth. If the same medial vertex is the apex of two up teeth, do not + cut that shared apex vertex. \end{enumerate} \end{definition} +\begin{remark}[Entry and shared up-apex exceptions] +\label{rem:up-apex-cut-exceptions} +For a single full medial tire graph there is one entry tooth. In a +chained tire decomposition each tread has its own entry tooth, inherited +from the parent side or chosen at the root. These entry triangles are +left uncut. Shared up-apex vertices are also left uncut: the intended +cut set contains the apexes of singleton up teeth only. +\end{remark} + \begin{remark}[Closing tooth of a descended face] \label{rem:closing-tooth} For the entry face the traversal of step (2) closes by returning to the @@ -156,9 +168,9 @@ three down teeth and closes on the already-labelled bite tooth of edge $0$, so step (3) cuts by duplicating the annular vertex $a_1$ (Remark~\ref{rem:closing-tooth}). All eight teeth are now labelled, and -the two cuts leave only the outer face and the eight teeth as -$3$-faces. The labelling and cuts are produced by the script -\texttt{experiments/medial\_tire\_cut\_labelling.py}. +the closing cuts are followed by apex cuts at the non-entry up teeth on +edges $6$ and $7$. The labelling and cuts are produced by the script +\texttt{lib/medial\_tire\_cut\_labelling.py}. \end{example} \begin{figure}[h]