Augment same-level faces before medial tire extraction

This commit is contained in:
2026-06-15 14:35:13 -04:00
parent 5829938ab0
commit 464335082d
10 changed files with 138 additions and 42 deletions
@@ -1,19 +1,25 @@
# Random medial tire decomposition 1 # Random medial tire decomposition 1
- vertices: 30 - original vertices: 30
- edges: 84 - original edges: 84
- node connectivity: 5 - original node connectivity: 5
- augmented vertices: 33
- augmented edges: 93
- same-level faces filled: 3
- source vertex: 14 - source vertex: 14
- tire-tree nodes: 4 - tire-tree nodes: 4
- tire-tree edges: 3 - tire-tree edges: 3
| node | depth | faces | annular | up | down | bites | full medial tires | | node | depth | faces | annular | up | singleton down | bite apexes | full medial tires |
|--:|--:|--:|--:|--:|--:|--:|--:| |--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 2 | 23 | 23 | 10 | 13 | 0 | 1 | | T0 | 2 | 23 | 23 | 10 | 13 | 0 | 1 |
| T0.0 | | | | 10 | 13 | - | `UDDUDUUDDUDUDUDUDDDUUDD` | | T0.0 | | | | 10 | 13 | - | `UDDUDUUDDUDUDUDUDDDUUDD` |
| T1 | 1 | 15 | 15 | 5 | 10 | 0 | 1 | | T1 | 1 | 15 | 15 | 5 | 10 | 0 | 1 |
| T1.0 | | | | 5 | 10 | - | `UDDDUDDUDUDDDUD` | | T1.0 | | | | 5 | 10 | - | `UDDDUDDUDUDDDUD` |
| T2 | 3 | 8 | 5 | 11 | 0 | 0 | 1 | | T2 | 3 | 14 | 14 | 11 | 0 | 0 | 4 |
| T2.0 | | | | 5 | 0 | - | `UUUUU` | | T2.0 | | | | 3 | 0 | - | `UUU` |
| T2.1 | | | | 3 | 0 | - | `UUU` |
| T2.2 | | | | 5 | 0 | - | `UUUUU` |
| T2.3 | | | | 3 | 0 | - | `UUU` |
| T3 | 3 | 5 | 5 | 5 | 0 | 0 | 1 | | T3 | 3 | 5 | 5 | 5 | 0 | 0 | 1 |
| T3.0 | | | | 5 | 0 | - | `UUUUU` | | T3.0 | | | | 5 | 0 | - | `UUUUU` |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 376 KiB

@@ -52,6 +52,13 @@ class TreadNode:
tires: tuple[tuple[FullMedialTireGraph, dict], ...] tires: tuple[tuple[FullMedialTireGraph, dict], ...]
@dataclass(frozen=True)
class Augmentation:
graph: nx.Graph
added_vertices: tuple[int, ...]
filled_faces: tuple[tuple[int, tuple[int, int, int], int], ...]
def sample_plantri_graphs(n: int, count: int, seed: int, scan_limit: int) -> list[nx.Graph]: def sample_plantri_graphs(n: int, count: int, seed: int, scan_limit: int) -> list[nx.Graph]:
cmd = [str(REPO_ROOT / "plantri" / "plantri"), "-g", "-c5", str(n)] cmd = [str(REPO_ROOT / "plantri" / "plantri"), "-g", "-c5", str(n)]
rng = random.Random(seed) rng = random.Random(seed)
@@ -111,6 +118,39 @@ def edge_face_data(faces):
return face_edges, edge_faces return face_edges, edge_faces
def augment_same_level_faces(g: nx.Graph, source: int) -> Augmentation:
"""Stack a new vertex into every facial triangle with one BFS level.
If a triangular face has all three vertices at level d, the new vertex is
adjacent to those three vertices and therefore has level d + 1. This turns
the same-level region into three adjacent-level tread faces before the tire
decomposition is extracted.
"""
levels = nx.single_source_shortest_path_length(g, source)
faces = triangular_faces(g)
augmented = g.copy()
next_vertex = max(augmented.nodes()) + 1
added = []
filled = []
for face in faces:
face_levels = {levels[v] for v in face}
if len(face_levels) != 1:
continue
new_vertex = next_vertex
next_vertex += 1
augmented.add_node(new_vertex)
augmented.add_edges_from((new_vertex, v) for v in face)
added.append(new_vertex)
filled.append((new_vertex, tuple(face), next(iter(face_levels))))
return Augmentation(
graph=augmented,
added_vertices=tuple(added),
filled_faces=tuple(filled),
)
def depth_components(faces, face_edges, edge_faces, levels): def depth_components(faces, face_edges, edge_faces, levels):
depths = [min(levels[v] for v in face) for face in faces] depths = [min(levels[v] for v in face) for face in faces]
dual_adj: dict[int, set[int]] = defaultdict(set) dual_adj: dict[int, set[int]] = defaultdict(set)
@@ -169,10 +209,12 @@ def tread_from_component(faces, levels, face_indices):
} }
def build_tire_tree(g: nx.Graph, source: int): def build_tire_tree(g: nx.Graph, source: int, augment: bool = True):
faces = triangular_faces(g) augmentation = augment_same_level_faces(g, source) if augment else Augmentation(g, (), ())
work_graph = augmentation.graph
faces = triangular_faces(work_graph)
face_edges, edge_faces = edge_face_data(faces) face_edges, edge_faces = edge_face_data(faces)
levels = nx.single_source_shortest_path_length(g, source) levels = nx.single_source_shortest_path_length(work_graph, source)
comps, depths, dual_adj = depth_components(faces, face_edges, edge_faces, levels) comps, depths, dual_adj = depth_components(faces, face_edges, edge_faces, levels)
comp_of_face = {} comp_of_face = {}
for comp_idx, (_depth, face_indices) in enumerate(comps): for comp_idx, (_depth, face_indices) in enumerate(comps):
@@ -215,7 +257,7 @@ def build_tire_tree(g: nx.Graph, source: int):
parent_candidates.add(comp_to_node[other_comp]) parent_candidates.add(comp_to_node[other_comp])
for parent in parent_candidates: for parent in parent_candidates:
tree_edges.add((parent, child)) tree_edges.add((parent, child))
return faces, levels, nodes, sorted(tree_edges) return augmentation, faces, levels, nodes, sorted(tree_edges)
def graph_layout(g: nx.Graph): def graph_layout(g: nx.Graph):
@@ -225,24 +267,37 @@ def graph_layout(g: nx.Graph):
return nx.spring_layout(g, seed=0) return nx.spring_layout(g, seed=0)
def draw_base_graph(ax, g, levels, source): def draw_base_graph(ax, g, levels, source, added_vertices=()):
pos = graph_layout(g) pos = graph_layout(g)
max_level = max(levels.values()) max_level = max(levels.values())
cmap = plt.get_cmap("viridis", max_level + 1) cmap = plt.get_cmap("viridis", max_level + 1)
node_colors = [cmap(levels[v]) for v in g.nodes()] node_colors = [cmap(levels[v]) for v in g.nodes()]
nx.draw_networkx_edges(g, pos, ax=ax, edge_color="#cbd5e1", width=0.8) nx.draw_networkx_edges(g, pos, ax=ax, edge_color="#cbd5e1", width=0.8)
added_set = set(added_vertices)
nx.draw_networkx_nodes( nx.draw_networkx_nodes(
g, g,
pos, pos,
ax=ax, ax=ax,
node_color=node_colors, node_color=node_colors,
node_size=[150 if v == source else 72 for v in g.nodes()], node_size=[
edgecolors=["#dc2626" if v == source else "#111827" for v in g.nodes()], 150 if v == source else 96 if v in added_set else 72
linewidths=[1.8 if v == source else 0.45 for v in g.nodes()], for v in g.nodes()
],
edgecolors=[
"#dc2626" if v == source else "#7c3aed" if v in added_set else "#111827"
for v in g.nodes()
],
linewidths=[
1.8 if v == source else 1.2 if v in added_set else 0.45
for v in g.nodes()
],
) )
labels = {v: str(v) for v in g.nodes()} labels = {v: str(v) for v in g.nodes()}
nx.draw_networkx_labels(g, pos, labels=labels, ax=ax, font_size=5) nx.draw_networkx_labels(g, pos, labels=labels, ax=ax, font_size=5)
ax.set_title(f"G, source {source}; vertex levels 0..{max_level}", fontsize=10) ax.set_title(
f"Augmented G, source {source}; vertex levels 0..{max_level}",
fontsize=10,
)
ax.set_aspect("equal") ax.set_aspect("equal")
ax.axis("off") ax.axis("off")
@@ -374,52 +429,69 @@ def draw_medial_tire_grid(fig, outer_spec, nodes):
ax.axis("off") ax.axis("off")
def write_index(path: Path, graph_idx: int, source: int, g: nx.Graph, nodes, tree_edges): def write_index(
path: Path,
graph_idx: int,
source: int,
original_graph: nx.Graph,
augmentation: Augmentation,
nodes,
tree_edges,
):
g = augmentation.graph
lines = [ lines = [
f"# Random medial tire decomposition {graph_idx}", f"# Random medial tire decomposition {graph_idx}",
"", "",
f"- vertices: {g.number_of_nodes()}", f"- original vertices: {original_graph.number_of_nodes()}",
f"- edges: {g.number_of_edges()}", f"- original edges: {original_graph.number_of_edges()}",
f"- node connectivity: {nx.node_connectivity(g)}", f"- original node connectivity: {nx.node_connectivity(original_graph)}",
f"- augmented vertices: {g.number_of_nodes()}",
f"- augmented edges: {g.number_of_edges()}",
f"- same-level faces filled: {len(augmentation.added_vertices)}",
f"- source vertex: {source}", f"- source vertex: {source}",
f"- tire-tree nodes: {len(nodes)}", f"- tire-tree nodes: {len(nodes)}",
f"- tire-tree edges: {len(tree_edges)}", f"- tire-tree edges: {len(tree_edges)}",
"", "",
"| node | depth | faces | annular | up | down | bites | full medial tires |", "| node | depth | faces | annular | up | singleton down | bite apexes | full medial tires |",
"|--:|--:|--:|--:|--:|--:|--:|--:|", "|--:|--:|--:|--:|--:|--:|--:|--:|",
] ]
for node in nodes: for node in nodes:
singleton_down = set(node.down) - set(node.bites)
lines.append( lines.append(
f"| T{node.idx} | {node.depth} | {len(node.face_indices)} | " f"| T{node.idx} | {node.depth} | {len(node.face_indices)} | "
f"{len(node.annular)} | {len(node.up)} | {len(node.down)} | " f"{len(node.annular)} | {len(node.up)} | {len(singleton_down)} | "
f"{len(node.bites)} | {len(node.tires)} |" f"{len(node.bites)} | {len(node.tires)} |"
) )
for comp_idx, (tire, _bij) in enumerate(node.tires): for comp_idx, (tire, _bij) in enumerate(node.tires):
bites = ",".join(f"({i},{j})" for i, j in sorted(tire.bites)) or "-" bites = ",".join(f"({i},{j})" for i, j in sorted(tire.bites)) or "-"
lines.append( lines.append(
f"| T{node.idx}.{comp_idx} | | | | {len(tire.up_edges)} | " f"| T{node.idx}.{comp_idx} | | | | {len(tire.up_edges)} | "
f"{len(tire.down_edges)} | {bites} | `{tire.tooth_word}` |" f"{len(tire.singleton_down_edges)} | {bites} | `{tire.tooth_word}` |"
) )
path.write_text("\n".join(lines) + "\n") path.write_text("\n".join(lines) + "\n")
def draw_case(out_dir: Path, graph_idx: int, g: nx.Graph, source: int): def draw_case(out_dir: Path, graph_idx: int, g: nx.Graph, source: int, augment: bool = True):
_faces, levels, nodes, tree_edges = build_tire_tree(g, source) augmentation, _faces, levels, nodes, tree_edges = build_tire_tree(g, source, augment=augment)
work_graph = augmentation.graph
fig = plt.figure(figsize=(17, 10)) fig = plt.figure(figsize=(17, 10))
spec = fig.add_gridspec(2, 2, width_ratios=[1.15, 1.0], height_ratios=[1.0, 1.25]) spec = fig.add_gridspec(2, 2, width_ratios=[1.15, 1.0], height_ratios=[1.0, 1.25])
ax_graph = fig.add_subplot(spec[0, 0]) ax_graph = fig.add_subplot(spec[0, 0])
ax_tree = fig.add_subplot(spec[1, 0]) ax_tree = fig.add_subplot(spec[1, 0])
draw_base_graph(ax_graph, g, levels, source) draw_base_graph(ax_graph, work_graph, levels, source, augmentation.added_vertices)
draw_tire_tree(ax_tree, nodes, tree_edges) draw_tire_tree(ax_tree, nodes, tree_edges)
draw_medial_tire_grid(fig, spec[:, 1], nodes) draw_medial_tire_grid(fig, spec[:, 1], nodes)
fig.suptitle( fig.suptitle(
f"Random 5-connected maximal planar graph {graph_idx}: " f"Random 5-connected maximal planar graph {graph_idx}: "
f"n={g.number_of_nodes()}, source={source}", f"n={g.number_of_nodes()} (+{len(augmentation.added_vertices)}), "
f"source={source}",
fontsize=13, fontsize=13,
) )
legend = [ legend = [
Line2D([0], [0], marker="o", color="w", label="source", Line2D([0], [0], marker="o", color="w", label="source",
markerfacecolor="#fde68a", markeredgecolor="#dc2626", markersize=8), markerfacecolor="#fde68a", markeredgecolor="#dc2626", markersize=8),
Line2D([0], [0], marker="o", color="w", label="inserted vertex",
markerfacecolor="#fde68a", markeredgecolor="#7c3aed", markersize=8),
Line2D([0], [0], color="black", lw=1.3, label="annular cycle A(T)"), Line2D([0], [0], color="black", lw=1.3, label="annular cycle A(T)"),
Line2D([0], [0], marker="o", color="w", label="up tooth", Line2D([0], [0], marker="o", color="w", label="up tooth",
markerfacecolor="#2563eb", markersize=6), markerfacecolor="#2563eb", markersize=6),
@@ -441,6 +513,7 @@ def draw_case(out_dir: Path, graph_idx: int, g: nx.Graph, source: int):
graph_idx, graph_idx,
source, source,
g, g,
augmentation,
nodes, nodes,
tree_edges, tree_edges,
) )
@@ -462,7 +535,9 @@ def run(args: argparse.Namespace):
sources = [rng.choice(list(graph.nodes())) for graph in graphs] sources = [rng.choice(list(graph.nodes())) for graph in graphs]
for i, (graph, source) in enumerate(zip(graphs, sources), start=1): for i, (graph, source) in enumerate(zip(graphs, sources), start=1):
png, pdf, node_count, tire_count = draw_case(out_dir, i, graph, source) png, pdf, node_count, tire_count = draw_case(
out_dir, i, graph, source, augment=not args.no_augment_same_level_faces
)
print( print(
f"case {i}: source={source}, connectivity={nx.node_connectivity(graph)}, " f"case {i}: source={source}, connectivity={nx.node_connectivity(graph)}, "
f"tire nodes={node_count}, full medial tires={tire_count}" f"tire nodes={node_count}, full medial tires={tire_count}"
@@ -479,6 +554,11 @@ def main():
parser.add_argument("--scan-limit", type=int, default=500) parser.add_argument("--scan-limit", type=int, default=500)
parser.add_argument("--graph6", help="draw this graph6 graph instead of sampling") parser.add_argument("--graph6", help="draw this graph6 graph instead of sampling")
parser.add_argument("--source", type=int, help="source vertex for --graph6") parser.add_argument("--source", type=int, help="source vertex for --graph6")
parser.add_argument(
"--no-augment-same-level-faces",
action="store_true",
help="skip the same-level-face vertex insertion step",
)
parser.add_argument( parser.add_argument(
"--out-dir", "--out-dir",
default=str(PAPER_DIR / "experiments" / "random_medial_tire_decompositions"), default=str(PAPER_DIR / "experiments" / "random_medial_tire_decompositions"),
@@ -1,18 +1,22 @@
# Random medial tire decomposition 1 # Random medial tire decomposition 1
- vertices: 30 - original vertices: 30
- edges: 84 - original edges: 84
- node connectivity: 5 - original node connectivity: 5
- augmented vertices: 31
- augmented edges: 87
- same-level faces filled: 1
- source vertex: 9 - source vertex: 9
- tire-tree nodes: 3 - tire-tree nodes: 3
- tire-tree edges: 2 - tire-tree edges: 2
| node | depth | faces | annular | up | down | bites | full medial tires | | node | depth | faces | annular | up | singleton down | bite apexes | full medial tires |
|--:|--:|--:|--:|--:|--:|--:|--:| |--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 1 | 16 | 16 | 6 | 10 | 0 | 1 | | T0 | 1 | 16 | 16 | 6 | 10 | 0 | 1 |
| T0.0 | | | | 6 | 10 | - | `DUDUDUDDUDDDUDDU` | | T0.0 | | | | 6 | 10 | - | `DUDUDUDDUDDDUDDU` |
| T1 | 2 | 20 | 20 | 10 | 10 | 0 | 1 | | T1 | 2 | 20 | 20 | 10 | 10 | 0 | 1 |
| T1.0 | | | | 10 | 10 | - | `UUDUUDUDDUDUDUDUUDDD` | | T1.0 | | | | 10 | 10 | - | `UUDUUDUDDUDUDUDUUDDD` |
| T2 | 3 | 14 | 13 | 12 | 1 | 1 | 2 | | T2 | 3 | 16 | 16 | 12 | 0 | 1 | 3 |
| T2.0 | | | | 6 | 2 | (1,5) | `UDUUUDUU` | | T2.0 | | | | 6 | 0 | (1,5) | `UDUUUDUU` |
| T2.1 | | | | 5 | 0 | - | `UUUUU` | | T2.1 | | | | 3 | 0 | - | `UUU` |
| T2.2 | | | | 5 | 0 | - | `UUUUU` |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 326 KiB

@@ -1,17 +1,23 @@
# Random medial tire decomposition 2 # Random medial tire decomposition 2
- vertices: 30 - original vertices: 30
- edges: 84 - original edges: 84
- node connectivity: 5 - original node connectivity: 5
- augmented vertices: 32
- augmented edges: 90
- same-level faces filled: 2
- source vertex: 4 - source vertex: 4
- tire-tree nodes: 3 - tire-tree nodes: 4
- tire-tree edges: 2 - tire-tree edges: 3
| node | depth | faces | annular | up | down | bites | full medial tires | | node | depth | faces | annular | up | singleton down | bite apexes | full medial tires |
|--:|--:|--:|--:|--:|--:|--:|--:| |--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 1 | 16 | 16 | 7 | 9 | 0 | 1 | | T0 | 1 | 16 | 16 | 7 | 9 | 0 | 1 |
| T0.0 | | | | 7 | 9 | - | `DUDUDUDUDUDDUDUD` | | T0.0 | | | | 7 | 9 | - | `DUDUDUDUDUDDUDUD` |
| T1 | 2 | 17 | 17 | 9 | 8 | 0 | 1 | | T1 | 2 | 17 | 17 | 9 | 8 | 0 | 1 |
| T1.0 | | | | 9 | 8 | - | `UUUDDUDUDUDUDDUUD` | | T1.0 | | | | 9 | 8 | - | `UUUDDUDUDUDUDDUUD` |
| T2 | 3 | 14 | 14 | 8 | 5 | 1 | 1 | | T2 | 3 | 14 | 14 | 8 | 4 | 1 | 1 |
| T2.0 | | | | 8 | 6 | (1,5) | `DDUUUDDUUDUDUU` | | T2.0 | | | | 8 | 4 | (1,5) | `DDUUUDDUUDUDUU` |
| T3 | 4 | 6 | 6 | 5 | 0 | 0 | 2 |
| T3.0 | | | | 3 | 0 | - | `UUU` |
| T3.1 | | | | 3 | 0 | - | `UUU` |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 331 KiB