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 d2f6901..9cf0ca9 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 @@ -304,6 +304,130 @@ def draw_png(result, path, scale=6.0): plt.close(fig) +def _tire_coords(g, r_ann=1.0, r_up=1.46, r_down=0.60): + """Annular/teeth coordinates for one tread, matching + ``medial_tire_cut_labelling.to_tikz``: a_0 at the top, k increasing CW.""" + import math + n = g.n + + def ang(k): + return math.radians(90.0 - k * 360.0 / n) + + def mid(i): + 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 g.up_edges: + a = mid(i) + pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a)) + for i in g.singleton_down_edges: + a = mid(i) + pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a)) + for (i, j) in g.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 _draw_tread(ax, g, depth, cuts, entry_edge, title): + """Draw one full medial tire cut on ``ax`` (annular cycle, teeth, walk-depth + labels, cut slits), mirroring ``medial_tire_cut_labelling.to_tikz``.""" + import math + n = g.n + pos = _tire_coords(g) + + def seg(a, b, **kw): + ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]], **kw) + + # annular cycle + xs = [pos[f"a{k}"][0] for k in range(n)] + [pos["a0"][0]] + ys = [pos[f"a{k}"][1] for k in range(n)] + [pos["a0"][1]] + ax.plot(xs, ys, color="black", lw=1.4, zorder=1) + # spokes (teeth) + for i in g.up_edges: + seg(f"u{i}", f"a{i}", color="0.55", lw=0.6, zorder=1) + seg(f"u{i}", f"a{(i + 1) % n}", color="0.55", lw=0.6, zorder=1) + for i in g.singleton_down_edges: + seg(f"d{i}", f"a{i}", color="0.55", lw=0.6, zorder=1) + seg(f"d{i}", f"a{(i + 1) % n}", color="0.55", lw=0.6, zorder=1) + for (i, j) in g.bites: + apex = f"p{i}_{j}" + for e in (i, j): + seg(apex, f"a{e}", color="0.55", lw=0.6, zorder=1) + seg(apex, f"a{(e + 1) % n}", color="0.55", lw=0.6, zorder=1) + # vertices + for k in range(n): + ax.plot(*pos[f"a{k}"], "o", ms=3, color="black", zorder=3) + for i in g.up_edges: + ax.plot(*pos[f"u{i}"], "o", ms=5, mfc="#cfe0f3", mec="#3a6ea5", zorder=3) + for i in g.singleton_down_edges: + ax.plot(*pos[f"d{i}"], "o", ms=5, mfc="#f3cfcf", mec="#a53a3a", zorder=3) + for (i, j) in g.bites: + ax.plot(*pos[f"p{i}_{j}"], "o", ms=6, mfc="#e8a0a0", mec="#a53a3a", zorder=3) + # walk-depth labels along each spoke + if depth is not None: + for edge in range(n): + ax_, ay = pos[g.apex_of_edge(edge)] + a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"] + mx, my = 0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]) + lx, ly = ax_ + 0.5 * (mx - ax_), ay + 0.5 * (my - ay) + ax.text(lx, ly, str(depth[edge]), fontsize=7, fontweight="bold", + ha="center", va="center", zorder=4) + # cut slits + for c in cuts or []: + 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) + ax.plot([vx - dx, vx + dx], [vy - dy, vy + dy], + color="#cc2020", lw=2.0, zorder=5) + ax.text(vx + 0.34 * math.cos(rad), vy + 0.34 * math.sin(rad), + f"cut {c.order + 1}", fontsize=6, color="#cc2020", + ha="center", va="center", zorder=5) + # entry marker + if entry_edge is not None: + ex, ey = pos[g.apex_of_edge(entry_edge)] + rad = math.atan2(ey, ex) + ax.text(ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad), + "entry", fontsize=6, color="#3a6ea5", ha="center", va="center") + ax.set_title(title, fontsize=8) + ax.set_aspect("equal") + ax.axis("off") + + +def draw_tire_cuts_png(result, path): + """Render every recognised tread's full medial tire cut, one panel each.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + res = result["results"] + depths = sorted(res) + if not depths: + raise ValueError("no recognised treads to draw") + fig, axes = plt.subplots(1, len(depths), figsize=(5.2 * len(depths), 5.4)) + if len(depths) == 1: + axes = [axes] + for ax, d in zip(axes, depths): + rec = res[d] + g = rec["g"] + title = (f"tread {d}: |A(T)|={g.n} word={g.tooth_word}\n" + f"bites={sorted(g.bites)} entry=e{rec['entry_edge']} " + f"start_depth={rec['start_depth']} cuts={len(rec['cuts'])}") + _draw_tread(ax, g, rec["depth"], rec["cuts"], rec["entry_edge"], title) + fig.suptitle(f"full medial tire cuts -- source {result['source']}, " + f"root entry e{result['entry_edge']}", fontsize=10) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + + def main(): parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) @@ -315,6 +439,8 @@ def main(): parser.add_argument("--entry", type=int, default=None, help="fix the root entry tooth (requires --source)") parser.add_argument("--png", metavar="PATH", help="render the dual cut to PNG") + parser.add_argument("--tire-png", metavar="PATH", + help="render each full medial tire cut to PNG") args = parser.parse_args() rng = random.Random(args.seed) @@ -338,6 +464,9 @@ def main(): if args.png: draw_png(result, args.png) print(f"wrote {args.png}") + if args.tire_png: + draw_tire_cuts_png(result, args.tire_png) + print(f"wrote {args.tire_png}") if __name__ == "__main__": diff --git a/papers/medial_tire_cuts/funcD_seed7_tire_cuts.png b/papers/medial_tire_cuts/funcD_seed7_tire_cuts.png new file mode 100644 index 0000000..286871d Binary files /dev/null and b/papers/medial_tire_cuts/funcD_seed7_tire_cuts.png differ