Add figures, Kempe-cycle section, and restriction experiments
Adds two TikZ figures (boundary-state worst cases and annular cycle counterexample), a new subsection on Kempe-cycle conservation across medial tires, and the experiment scripts/findings for the medial tire restriction search and annular cycle condition check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
||||
"""Check the medial annular-cycle almost-two-colour condition.
|
||||
|
||||
For each generated plane triangulation G and each requested level source:
|
||||
|
||||
1. Build the full medial graph M(G).
|
||||
2. Find depth-component tire annular medial subgraphs.
|
||||
3. Enumerate simple cycles in those annular subgraphs.
|
||||
4. Search for a proper vertex 3-colouring of M(G) such that every
|
||||
such cycle uses two colours except at at most one vertex.
|
||||
|
||||
Run with Sage, for example:
|
||||
|
||||
sage -python papers/medial_tire_decompositions_of_plane_triangulations/experiments/check_medial_annular_cycle_condition.py --n-min 4 --n-max 8
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict, deque
|
||||
from itertools import combinations
|
||||
from typing import Any, Iterable, Iterator, Sequence, cast
|
||||
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from sage.graphs.graph_coloring import all_graph_colorings # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
|
||||
|
||||
Edge = tuple[Any, Any]
|
||||
Coloring = dict[Edge, int]
|
||||
Source = tuple[Any, ...]
|
||||
|
||||
|
||||
def vertex_key(v: Any) -> str:
|
||||
return repr(v)
|
||||
|
||||
|
||||
def edge_key(u: Any, v: Any) -> Edge:
|
||||
return (u, v) if vertex_key(u) <= vertex_key(v) else (v, u)
|
||||
|
||||
|
||||
def is_induced_cycle(g: Graph, vertices: Sequence[Any]) -> bool:
|
||||
if len(vertices) < 3:
|
||||
return False
|
||||
h = cast(Graph, g.subgraph(list(vertices)))
|
||||
return h.is_connected() and h.num_edges() == len(vertices) and all(
|
||||
h.degree(v) == 2 for v in h.vertices()
|
||||
)
|
||||
|
||||
|
||||
def induced_cycle_sources(g: Graph, max_size: int | None = None) -> Iterator[Source]:
|
||||
vertices = sorted(g.vertices(), key=vertex_key)
|
||||
upper = len(vertices) if max_size is None else min(max_size, len(vertices))
|
||||
for k in range(3, upper + 1):
|
||||
for subset in combinations(vertices, k):
|
||||
if is_induced_cycle(g, subset):
|
||||
yield tuple(subset)
|
||||
|
||||
|
||||
def level_sources(g: Graph, mode: str, max_cycle_source_size: int | None) -> Iterator[Source]:
|
||||
if mode in ("vertex", "all"):
|
||||
for v in sorted(g.vertices(), key=vertex_key):
|
||||
yield (v,)
|
||||
if mode in ("cycle", "all"):
|
||||
yield from induced_cycle_sources(g, max_cycle_source_size)
|
||||
|
||||
|
||||
def distances_from_source(g: Graph, source: Source) -> dict[Any, int]:
|
||||
if len(source) == 1:
|
||||
return dict(g.shortest_path_lengths(source[0]))
|
||||
distances = {v: 0 for v in source}
|
||||
queue: deque[Any] = deque(source)
|
||||
while queue:
|
||||
v = queue.popleft()
|
||||
for w in g.neighbor_iterator(v):
|
||||
if w in distances:
|
||||
continue
|
||||
distances[w] = distances[v] + 1
|
||||
queue.append(w)
|
||||
return distances
|
||||
|
||||
|
||||
def embedded_copy(g: Graph) -> Graph:
|
||||
emb = cast(Graph, g.copy())
|
||||
if not emb.is_planar(set_embedding=True):
|
||||
raise ValueError("graph is not planar")
|
||||
return emb
|
||||
|
||||
|
||||
def medial_graph(g: Graph) -> Graph:
|
||||
"""Build the full medial graph from the embedding rotation at vertices."""
|
||||
emb = embedded_copy(g)
|
||||
rotation = emb.get_embedding()
|
||||
m = Graph()
|
||||
medial_vertices = [edge_key(u, v) for u, v, _ in emb.edge_iterator()]
|
||||
m.add_vertices(medial_vertices)
|
||||
for v, neighbors in rotation.items():
|
||||
if len(neighbors) < 2:
|
||||
continue
|
||||
n = len(neighbors)
|
||||
for i in range(n):
|
||||
e1 = edge_key(v, neighbors[i])
|
||||
e2 = edge_key(v, neighbors[(i + 1) % n])
|
||||
if e1 != e2:
|
||||
m.add_edge(e1, e2)
|
||||
return m
|
||||
|
||||
|
||||
def face_vertices(face: Sequence[tuple[Any, Any]]) -> set[Any]:
|
||||
out: set[Any] = set()
|
||||
for u, v in face:
|
||||
out.add(u)
|
||||
out.add(v)
|
||||
return out
|
||||
|
||||
|
||||
def face_edges(face: Sequence[tuple[Any, Any]]) -> set[Edge]:
|
||||
return {edge_key(u, v) for u, v in face}
|
||||
|
||||
|
||||
def dual_components_by_depth(
|
||||
g: Graph, source: Source
|
||||
) -> list[tuple[int, list[int], set[Edge]]]:
|
||||
"""Return (depth, face-indices, annular-edge-set) for each depth component."""
|
||||
emb = embedded_copy(g)
|
||||
distances = distances_from_source(emb, source)
|
||||
faces = emb.faces()
|
||||
f_vertices = [face_vertices(face) for face in faces]
|
||||
f_edges = [face_edges(face) for face in faces]
|
||||
depths = [min(distances[v] for v in verts) for verts in f_vertices]
|
||||
|
||||
edge_faces: dict[Edge, list[int]] = defaultdict(list)
|
||||
for idx, edges in enumerate(f_edges):
|
||||
for edge in edges:
|
||||
edge_faces[edge].append(idx)
|
||||
|
||||
dual_adj: dict[int, set[int]] = defaultdict(set)
|
||||
for incident in edge_faces.values():
|
||||
for a in range(len(incident)):
|
||||
for b in range(a + 1, len(incident)):
|
||||
dual_adj[incident[a]].add(incident[b])
|
||||
dual_adj[incident[b]].add(incident[a])
|
||||
|
||||
components = []
|
||||
seen = [False] * len(faces)
|
||||
for start in range(len(faces)):
|
||||
if seen[start]:
|
||||
continue
|
||||
depth = depths[start]
|
||||
comp = [start]
|
||||
seen[start] = True
|
||||
stack = [start]
|
||||
while stack:
|
||||
f = stack.pop()
|
||||
for h in dual_adj[f]:
|
||||
if not seen[h] and depths[h] == depth:
|
||||
seen[h] = True
|
||||
comp.append(h)
|
||||
stack.append(h)
|
||||
|
||||
annular_edges: set[Edge] = set()
|
||||
for f in comp:
|
||||
for u, v in f_edges[f]:
|
||||
if {distances[u], distances[v]} == {depth, depth + 1}:
|
||||
annular_edges.add(edge_key(u, v))
|
||||
if len(annular_edges) >= 3:
|
||||
components.append((depth, comp, annular_edges))
|
||||
return components
|
||||
|
||||
|
||||
def simple_cycle_vertex_sets(g: Graph) -> set[frozenset[Any]]:
|
||||
vertices = sorted(g.vertices(), key=repr)
|
||||
index = {v: i for i, v in enumerate(vertices)}
|
||||
cycles: set[frozenset[Any]] = set()
|
||||
|
||||
def dfs(start: Any, current: Any, path: list[Any], seen: set[Any]) -> None:
|
||||
for nxt in g.neighbor_iterator(current):
|
||||
if nxt == start:
|
||||
if len(path) >= 3:
|
||||
cycles.add(frozenset(path))
|
||||
continue
|
||||
if nxt in seen or index[nxt] <= index[start]:
|
||||
continue
|
||||
seen.add(nxt)
|
||||
path.append(nxt)
|
||||
dfs(start, nxt, path, seen)
|
||||
path.pop()
|
||||
seen.remove(nxt)
|
||||
|
||||
for start in vertices:
|
||||
dfs(start, start, [start], {start})
|
||||
return cycles
|
||||
|
||||
|
||||
def annular_medial_cycles(g: Graph, source: Source) -> list[frozenset[Edge]]:
|
||||
m = medial_graph(g)
|
||||
cycles: list[frozenset[Edge]] = []
|
||||
seen: set[frozenset[Edge]] = set()
|
||||
for _depth, _faces, annular_edges in dual_components_by_depth(g, source):
|
||||
sub = cast(Graph, m.subgraph(list(annular_edges)))
|
||||
for cycle in simple_cycle_vertex_sets(sub):
|
||||
typed = frozenset(cast(Iterable[Edge], cycle))
|
||||
if typed not in seen:
|
||||
seen.add(typed)
|
||||
cycles.append(typed)
|
||||
return cycles
|
||||
|
||||
|
||||
def almost_two_coloured(cycle: frozenset[Edge], coloring: Coloring) -> bool:
|
||||
counts = defaultdict(int)
|
||||
for vertex in cycle:
|
||||
counts[coloring[vertex]] += 1
|
||||
return min(counts.get(c, 0) for c in range(3)) <= 1
|
||||
|
||||
|
||||
def first_cycle_violation(
|
||||
cycles: Sequence[frozenset[Edge]], coloring: Coloring
|
||||
) -> frozenset[Edge] | None:
|
||||
for cycle in cycles:
|
||||
if not almost_two_coloured(cycle, coloring):
|
||||
return cycle
|
||||
return None
|
||||
|
||||
|
||||
def color_counts(cycle: frozenset[Edge], coloring: Coloring) -> dict[int, int]:
|
||||
counts = {0: 0, 1: 0, 2: 0}
|
||||
for vertex in cycle:
|
||||
counts[coloring[vertex]] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def coloring_witness(
|
||||
m: Graph,
|
||||
cycles: Sequence[frozenset[Edge]],
|
||||
max_colorings: int | None,
|
||||
) -> tuple[Coloring | None, int, bool, frozenset[Edge] | None, Coloring | None]:
|
||||
checked = 0
|
||||
last_violation = None
|
||||
for raw in all_graph_colorings(m, 3, vertex_color_dict=True):
|
||||
coloring = cast(Coloring, raw)
|
||||
checked += 1
|
||||
violation = first_cycle_violation(cycles, coloring)
|
||||
if violation is None:
|
||||
return coloring, checked, True, None, None
|
||||
last_violation = violation
|
||||
if max_colorings is not None and checked >= max_colorings:
|
||||
return None, checked, False, last_violation, coloring
|
||||
return None, checked, True, last_violation, coloring
|
||||
|
||||
|
||||
def source_label(source: Source) -> str:
|
||||
if len(source) == 1:
|
||||
return f"vertex:{source[0]}"
|
||||
return "cycle:{" + ",".join(map(str, source)) + "}"
|
||||
|
||||
|
||||
def graphs_to_check(n: int, max_graphs: int | None):
|
||||
for idx, g in enumerate(graphs.triangulations(n)):
|
||||
if max_graphs is not None and idx >= max_graphs:
|
||||
break
|
||||
yield idx, cast(Graph, g)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
total_cases = 0
|
||||
skipped_no_cycles = 0
|
||||
witnesses = 0
|
||||
failures = []
|
||||
inconclusive = []
|
||||
|
||||
for n in range(args.n_min, args.n_max + 1):
|
||||
print(f"n={n}")
|
||||
for graph_idx, g in graphs_to_check(n, args.max_graphs_per_n):
|
||||
m = medial_graph(g)
|
||||
sources = list(level_sources(g, args.sources, args.max_cycle_source_size))
|
||||
if args.max_sources_per_graph is not None:
|
||||
sources = sources[: args.max_sources_per_graph]
|
||||
for source in sources:
|
||||
cycles = annular_medial_cycles(g, source)
|
||||
if not cycles:
|
||||
skipped_no_cycles += 1
|
||||
continue
|
||||
total_cases += 1
|
||||
witness, checked, exhausted, violation, last_coloring = coloring_witness(
|
||||
m, cycles, args.max_colorings
|
||||
)
|
||||
if witness is not None:
|
||||
witnesses += 1
|
||||
if args.verbose:
|
||||
print(
|
||||
f" graph={graph_idx} source={source_label(source)} "
|
||||
f"cycles={len(cycles)} witness_after={checked}"
|
||||
)
|
||||
continue
|
||||
record = {
|
||||
"n": n,
|
||||
"graph_idx": graph_idx,
|
||||
"graph_edges": sorted(edge_key(u, v) for u, v, _ in g.edge_iterator()),
|
||||
"source": source_label(source),
|
||||
"cycles": len(cycles),
|
||||
"checked": checked,
|
||||
"exhausted": exhausted,
|
||||
"violation_size": len(violation) if violation else None,
|
||||
}
|
||||
if args.failure_details and violation is not None and last_coloring is not None:
|
||||
record["violation_cycle"] = sorted(violation)
|
||||
record["violation_counts"] = color_counts(violation, last_coloring)
|
||||
record["violation_coloring"] = {
|
||||
edge: last_coloring[edge] for edge in sorted(violation)
|
||||
}
|
||||
if exhausted:
|
||||
failures.append(record)
|
||||
print(" FAILURE", record)
|
||||
if args.stop_on_failure:
|
||||
print_summary(total_cases, skipped_no_cycles, witnesses, failures, inconclusive)
|
||||
return
|
||||
else:
|
||||
inconclusive.append(record)
|
||||
print(" INCONCLUSIVE", record)
|
||||
|
||||
print_summary(total_cases, skipped_no_cycles, witnesses, failures, inconclusive)
|
||||
|
||||
|
||||
def print_summary(
|
||||
total_cases: int,
|
||||
skipped_no_cycles: int,
|
||||
witnesses: int,
|
||||
failures: Sequence[dict],
|
||||
inconclusive: Sequence[dict],
|
||||
) -> None:
|
||||
print()
|
||||
print("summary")
|
||||
print(f" checked source decompositions with annular cycles: {total_cases}")
|
||||
print(f" skipped source decompositions with no annular cycles: {skipped_no_cycles}")
|
||||
print(f" witnesses found: {witnesses}")
|
||||
print(f" failures: {len(failures)}")
|
||||
print(f" inconclusive: {len(inconclusive)}")
|
||||
if failures:
|
||||
print(f" first failure: {failures[0]}")
|
||||
if inconclusive:
|
||||
print(f" first inconclusive: {inconclusive[0]}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--n-min", type=int, default=4)
|
||||
parser.add_argument("--n-max", type=int, default=8)
|
||||
parser.add_argument("--sources", choices=("vertex", "cycle", "all"), default="vertex")
|
||||
parser.add_argument("--max-cycle-source-size", type=int, default=6)
|
||||
parser.add_argument("--max-graphs-per-n", type=int)
|
||||
parser.add_argument("--max-sources-per-graph", type=int)
|
||||
parser.add_argument("--max-colorings", type=int)
|
||||
parser.add_argument("--stop-on-failure", action="store_true")
|
||||
parser.add_argument("--failure-details", action="store_true")
|
||||
parser.add_argument("--verbose", action="store_true")
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# Medial Annular Cycle Condition Findings
|
||||
|
||||
Question: for an actual plane triangulation and a tire decomposition,
|
||||
does there exist a proper vertex 3-coloring of the full medial graph
|
||||
such that every medial annular simple cycle uses two colors except at
|
||||
at most one vertex?
|
||||
|
||||
The experiment is graph-level, not local-pattern-level:
|
||||
|
||||
1. Build the full medial graph `M(G)` from the embedding rotation.
|
||||
2. Enumerate proper vertex 3-colorings of `M(G)`.
|
||||
3. For each level-source tire decomposition, build the annular medial
|
||||
subgraph of each depth-component tire.
|
||||
4. Enumerate simple cycles in those annular medial subgraphs.
|
||||
5. Search for a proper medial coloring satisfying the almost-two-color
|
||||
condition on every such cycle.
|
||||
|
||||
## First Counterexample Found
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
sage -python papers/medial_tire_decompositions_of_plane_triangulations/experiments/check_medial_annular_cycle_condition.py --n-min 7 --n-max 7 --stop-on-failure --failure-details
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
n=7
|
||||
FAILURE {
|
||||
'n': 7,
|
||||
'graph_idx': 0,
|
||||
'graph_edges': [
|
||||
(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7),
|
||||
(2, 3), (2, 6), (2, 7), (3, 4), (3, 5), (3, 6),
|
||||
(4, 5), (5, 6), (6, 7)
|
||||
],
|
||||
'source': 'vertex:1',
|
||||
'cycles': 1,
|
||||
'checked': 6,
|
||||
'exhausted': True,
|
||||
'violation_size': 6,
|
||||
'violation_cycle': [
|
||||
(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)
|
||||
],
|
||||
'violation_counts': {0: 2, 1: 2, 2: 2},
|
||||
'violation_coloring': {
|
||||
(1, 2): 2, (1, 3): 1, (1, 4): 0,
|
||||
(1, 5): 2, (1, 6): 0, (1, 7): 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Interpretation: for the vertex-source decomposition at source `1`, the
|
||||
only annular medial simple cycle is the six medial vertices
|
||||
corresponding to the six edges incident to `1`. Sage enumerated all six
|
||||
proper vertex 3-colorings of the full medial graph. Every one violates
|
||||
the almost-two-color condition on that cycle; the displayed coloring has
|
||||
color counts `{0: 2, 1: 2, 2: 2}` on the annular cycle.
|
||||
|
||||
Thus the conjecture, in the form "for every possible tire decomposition
|
||||
there exists a proper medial 3-coloring satisfying the condition on all
|
||||
medial annular simple cycles", is false.
|
||||
|
||||
## Default Sweep
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
sage -python papers/medial_tire_decompositions_of_plane_triangulations/experiments/check_medial_annular_cycle_condition.py --n-min 4 --n-max 8
|
||||
```
|
||||
|
||||
Summary:
|
||||
|
||||
```text
|
||||
checked source decompositions with annular cycles: 168
|
||||
skipped source decompositions with no annular cycles: 0
|
||||
witnesses found: 111
|
||||
failures: 57
|
||||
inconclusive: 0
|
||||
```
|
||||
|
||||
Failures begin at `n=7` for vertex-source decompositions.
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# Medial Tire Restriction Findings
|
||||
|
||||
Question: among possible full medial tire graphs, which examples most
|
||||
restrict the inner boundary coloring relative to the outer boundary
|
||||
coloring?
|
||||
|
||||
## Model
|
||||
|
||||
The experiment uses the ambient full-medial tread model.
|
||||
|
||||
- Annular medial vertices form a cycle `A_0, ..., A_{n-1}`.
|
||||
- Each annular face contributes one boundary-medial vertex `B_i`.
|
||||
- `B_i` is adjacent to `A_i` and `A_{i+1}`.
|
||||
- `B_i` is tagged `O` or `I` according to whether the corresponding
|
||||
primal boundary edge lies on the outer or inner boundary.
|
||||
|
||||
Boundary states are quotient by color permutations only. Boundary
|
||||
order is fixed: rotations and reversals are not identified.
|
||||
|
||||
Inner chords of the inner outerplanar graph do not affect this ambient
|
||||
full medial tire graph, because they are not incident to annular tread
|
||||
faces.
|
||||
|
||||
## Metrics
|
||||
|
||||
For the quotient transfer relation
|
||||
|
||||
```text
|
||||
R_T subset outer_state_orbits x inner_state_orbits
|
||||
```
|
||||
|
||||
the script reports:
|
||||
|
||||
- `min_outer_extensions`: minimum number of compatible inner state
|
||||
orbits over realized outer state orbits.
|
||||
- `avg_outer_extensions`: average number of compatible inner state
|
||||
orbits over realized outer state orbits.
|
||||
- `relation_density_all`: `|R_T|` divided by all possible outer/inner
|
||||
state orbit pairs.
|
||||
- `blocked_outer_orbits`: number of outer boundary state orbits that
|
||||
are not compatible with any inner boundary state orbit.
|
||||
- `blocked_inner_orbits`: number of inner boundary state orbits that
|
||||
are not compatible with any outer boundary state orbit.
|
||||
|
||||
## Exhaustive Sweep Through 10 Faces
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
python3 papers/medial_tire_decompositions_of_plane_triangulations/experiments/medial_tire_restriction_search.py --exhaustive --min-faces 6 --max-faces 10 --limit 3
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
graphs analyzed: 1974
|
||||
boundary states quotient by color permutations only; no rotations or reversals
|
||||
|
||||
most restrictive by min_outer_extensions
|
||||
types=IIIOOO
|
||||
faces=6 outer=3 inner=3 outer_orbits=5 inner_orbits=5
|
||||
realized_outer=4 realized_inner=4 relation=10
|
||||
blocked_outer=1 (0.200) blocked_inner=1 (0.200)
|
||||
min_outer_extensions=1 avg_outer_extensions=2.500
|
||||
density_all=0.400000 density_realized_outer=0.500000
|
||||
worst_outer_states=((0, 1, 2),)
|
||||
types=IIOIOO
|
||||
faces=6 outer=3 inner=3 outer_orbits=5 inner_orbits=5
|
||||
realized_outer=5 realized_inner=5 relation=9
|
||||
blocked_outer=0 (0.000) blocked_inner=0 (0.000)
|
||||
min_outer_extensions=1 avg_outer_extensions=1.800
|
||||
density_all=0.360000 density_realized_outer=0.360000
|
||||
worst_outer_states=((0, 0, 1), (0, 1, 2))
|
||||
types=IIOOIO
|
||||
faces=6 outer=3 inner=3 outer_orbits=5 inner_orbits=5
|
||||
realized_outer=5 realized_inner=5 relation=9
|
||||
blocked_outer=0 (0.000) blocked_inner=0 (0.000)
|
||||
min_outer_extensions=1 avg_outer_extensions=1.800
|
||||
density_all=0.360000 density_realized_outer=0.360000
|
||||
worst_outer_states=((0, 1, 1), (0, 1, 2))
|
||||
|
||||
most restrictive by avg_outer_extensions
|
||||
types=IIOOOO
|
||||
faces=6 outer=4 inner=2 outer_orbits=14 inner_orbits=2
|
||||
realized_outer=8 realized_inner=2 relation=8
|
||||
blocked_outer=6 (0.429) blocked_inner=0 (0.000)
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.285714 density_realized_outer=0.500000
|
||||
types=IOIOOO
|
||||
faces=6 outer=4 inner=2 outer_orbits=14 inner_orbits=2
|
||||
realized_outer=9 realized_inner=2 relation=9
|
||||
blocked_outer=5 (0.357) blocked_inner=0 (0.000)
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.321429 density_realized_outer=0.500000
|
||||
types=IOOIOO
|
||||
faces=6 outer=4 inner=2 outer_orbits=14 inner_orbits=2
|
||||
realized_outer=9 realized_inner=2 relation=9
|
||||
blocked_outer=5 (0.357) blocked_inner=0 (0.000)
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.321429 density_realized_outer=0.500000
|
||||
|
||||
most restrictive by relation_density_all
|
||||
types=IIIIIIIIIO
|
||||
faces=10 outer=1 inner=9 outer_orbits=1 inner_orbits=3281
|
||||
realized_outer=1 realized_inner=171 relation=171
|
||||
blocked_outer=0 (0.000) blocked_inner=3110 (0.948)
|
||||
min_outer_extensions=171 avg_outer_extensions=171.000
|
||||
density_all=0.052118 density_realized_outer=0.052118
|
||||
```
|
||||
|
||||
## Random Sweep Through 14 Faces
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
python3 papers/medial_tire_decompositions_of_plane_triangulations/experiments/medial_tire_restriction_search.py --samples 5000 --max-faces 14 --limit 3
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
graphs analyzed: 3227
|
||||
|
||||
most restrictive by min_outer_extensions
|
||||
types=IIIOOO
|
||||
min_outer_extensions=1 avg_outer_extensions=2.500
|
||||
density_all=0.400000 density_realized_outer=0.500000
|
||||
types=IIOIOO
|
||||
min_outer_extensions=1 avg_outer_extensions=1.800
|
||||
density_all=0.360000 density_realized_outer=0.360000
|
||||
types=IIOOIO
|
||||
min_outer_extensions=1 avg_outer_extensions=1.800
|
||||
density_all=0.360000 density_realized_outer=0.360000
|
||||
|
||||
most restrictive by avg_outer_extensions
|
||||
types=IIOOOO
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.285714 density_realized_outer=0.500000
|
||||
types=IOIOOO
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.321429 density_realized_outer=0.500000
|
||||
types=IOOIOO
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.321429 density_realized_outer=0.500000
|
||||
|
||||
most restrictive by relation_density_all
|
||||
types=IIIOIIIIIIIIII
|
||||
faces=14 outer=1 inner=13 relation=2731
|
||||
min_outer_extensions=2731 avg_outer_extensions=2731.000
|
||||
density_all=0.010278 density_realized_outer=0.010278
|
||||
types=OOOOOOOIOOOOOO
|
||||
faces=14 outer=13 inner=1 relation=2731
|
||||
min_outer_extensions=1 avg_outer_extensions=1.000
|
||||
density_all=0.010278 density_realized_outer=1.000000
|
||||
types=IIIOOIIIIIIIII
|
||||
faces=14 outer=2 inner=12 relation=2048
|
||||
min_outer_extensions=683 avg_outer_extensions=1024.000
|
||||
density_all=0.011561 density_realized_outer=0.011561
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The most restrictive small examples for extension counts appear at six
|
||||
annular faces. Alternating or clustered `O/I` boundary-face patterns
|
||||
with a small boundary on one side can force each realized outer state
|
||||
to a single inner orbit.
|
||||
|
||||
The raw density metric favors highly unbalanced boundaries, such as one
|
||||
outer boundary state position and many inner positions. This may be a
|
||||
less useful notion of "worst case" unless boundary sizes are fixed or
|
||||
the density is normalized within a fixed `(outer, inner)` size class.
|
||||
|
||||
The blocked-state metrics make the asymmetry explicit. For instance,
|
||||
`IIIOOO` blocks one of the five possible outer state orbits, while
|
||||
`IIOIOO` and `IIOOIO` block none but still contain outer states with a
|
||||
unique compatible inner state orbit. Highly unbalanced cases can block
|
||||
most state orbits on the larger boundary simply because the annular
|
||||
cycle has far fewer colorings than the unconstrained ordered boundary.
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
"""Search boundary-state restrictions for possible full medial tire graphs.
|
||||
|
||||
Model.
|
||||
A full medial tire graph has an annular medial cycle A_0,...,A_{n-1}.
|
||||
Each annular face contributes one boundary-medial vertex B_i adjacent
|
||||
to A_i and A_{i+1}; B_i is tagged as outer or inner. This is the
|
||||
ambient tread-face model from the medial tire decomposition paper.
|
||||
|
||||
Boundary order is the cyclic order inherited from the annular face
|
||||
sequence. Boundary states are quotient by S_3 color relabeling only:
|
||||
no rotations or reversals are identified.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
Coloring = tuple[int, ...]
|
||||
|
||||
|
||||
def canonical_color_orbit(coloring: Coloring) -> Coloring:
|
||||
"""Canonical representative modulo color renaming, preserving order."""
|
||||
mapping: dict[int, int] = {}
|
||||
out = []
|
||||
for color in coloring:
|
||||
if color not in mapping:
|
||||
mapping[color] = len(mapping)
|
||||
out.append(mapping[color])
|
||||
return tuple(out)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def all_state_orbits(length: int) -> frozenset[Coloring]:
|
||||
return frozenset({
|
||||
canonical_color_orbit(tuple(colors))
|
||||
for colors in itertools.product(range(3), repeat=length)
|
||||
})
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def cycle_3_colorings(n: int) -> list[Coloring]:
|
||||
colors = [-1] * n
|
||||
out: list[Coloring] = []
|
||||
|
||||
def rec(idx: int) -> None:
|
||||
if idx == n:
|
||||
if colors[-1] != colors[0]:
|
||||
out.append(tuple(colors))
|
||||
return
|
||||
for color in range(3):
|
||||
if idx > 0 and colors[idx - 1] == color:
|
||||
continue
|
||||
if idx == n - 1 and colors[0] == color:
|
||||
continue
|
||||
colors[idx] = color
|
||||
rec(idx + 1)
|
||||
colors[idx] = -1
|
||||
|
||||
rec(0)
|
||||
return out
|
||||
|
||||
|
||||
def boundary_coloring_from_annular(
|
||||
annular_coloring: Coloring,
|
||||
face_types: str,
|
||||
boundary_type: str,
|
||||
) -> Coloring:
|
||||
values = []
|
||||
n = len(face_types)
|
||||
for idx, face_type in enumerate(face_types):
|
||||
if face_type != boundary_type:
|
||||
continue
|
||||
left = annular_coloring[idx]
|
||||
right = annular_coloring[(idx + 1) % n]
|
||||
values.append(({0, 1, 2} - {left, right}).pop())
|
||||
return tuple(values)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RestrictionMetrics:
|
||||
face_types: str
|
||||
n_faces: int
|
||||
n_outer: int
|
||||
n_inner: int
|
||||
outer_orbits: int
|
||||
inner_orbits: int
|
||||
realized_outer_orbits: int
|
||||
realized_inner_orbits: int
|
||||
blocked_outer_orbits: int
|
||||
blocked_inner_orbits: int
|
||||
blocked_outer_fraction: float
|
||||
blocked_inner_fraction: float
|
||||
relation_size: int
|
||||
min_outer_extensions: int
|
||||
avg_outer_extensions: float
|
||||
relation_density_all: float
|
||||
relation_density_realized_outer: float
|
||||
worst_outer_states: tuple[Coloring, ...]
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def analyze_face_types(face_types: str) -> RestrictionMetrics:
|
||||
n = len(face_types)
|
||||
n_outer = face_types.count("O")
|
||||
n_inner = face_types.count("I")
|
||||
possible_outer = all_state_orbits(n_outer)
|
||||
possible_inner = all_state_orbits(n_inner)
|
||||
|
||||
relation: set[tuple[Coloring, Coloring]] = set()
|
||||
by_outer: dict[Coloring, set[Coloring]] = defaultdict(set)
|
||||
|
||||
for annular in cycle_3_colorings(n):
|
||||
outer = canonical_color_orbit(
|
||||
boundary_coloring_from_annular(annular, face_types, "O")
|
||||
)
|
||||
inner = canonical_color_orbit(
|
||||
boundary_coloring_from_annular(annular, face_types, "I")
|
||||
)
|
||||
relation.add((outer, inner))
|
||||
by_outer[outer].add(inner)
|
||||
|
||||
realized_outer = set(by_outer)
|
||||
realized_inner = {inner for _, inner in relation}
|
||||
extension_counts = {outer: len(inners) for outer, inners in by_outer.items()}
|
||||
min_extensions = min(extension_counts.values()) if extension_counts else 0
|
||||
worst_outer = tuple(
|
||||
sorted(outer for outer, count in extension_counts.items() if count == min_extensions)
|
||||
)
|
||||
avg_extensions = (
|
||||
sum(extension_counts.values()) / len(extension_counts)
|
||||
if extension_counts
|
||||
else 0.0
|
||||
)
|
||||
all_denominator = len(possible_outer) * len(possible_inner)
|
||||
realized_denominator = len(realized_outer) * len(possible_inner)
|
||||
|
||||
return RestrictionMetrics(
|
||||
face_types=face_types,
|
||||
n_faces=n,
|
||||
n_outer=n_outer,
|
||||
n_inner=n_inner,
|
||||
outer_orbits=len(possible_outer),
|
||||
inner_orbits=len(possible_inner),
|
||||
realized_outer_orbits=len(realized_outer),
|
||||
realized_inner_orbits=len(realized_inner),
|
||||
blocked_outer_orbits=len(possible_outer) - len(realized_outer),
|
||||
blocked_inner_orbits=len(possible_inner) - len(realized_inner),
|
||||
blocked_outer_fraction=(
|
||||
(len(possible_outer) - len(realized_outer)) / len(possible_outer)
|
||||
if possible_outer
|
||||
else 0.0
|
||||
),
|
||||
blocked_inner_fraction=(
|
||||
(len(possible_inner) - len(realized_inner)) / len(possible_inner)
|
||||
if possible_inner
|
||||
else 0.0
|
||||
),
|
||||
relation_size=len(relation),
|
||||
min_outer_extensions=min_extensions,
|
||||
avg_outer_extensions=avg_extensions,
|
||||
relation_density_all=len(relation) / all_denominator if all_denominator else 0.0,
|
||||
relation_density_realized_outer=(
|
||||
len(relation) / realized_denominator if realized_denominator else 0.0
|
||||
),
|
||||
worst_outer_states=worst_outer[:5],
|
||||
)
|
||||
|
||||
|
||||
def random_face_types(rng: random.Random, n: int, min_outer: int, min_inner: int) -> str:
|
||||
if min_outer + min_inner > n:
|
||||
raise ValueError("minimum outer/inner counts exceed n")
|
||||
values = ["O"] * min_outer + ["I"] * min_inner
|
||||
values.extend(rng.choice("OI") for _ in range(n - len(values)))
|
||||
rng.shuffle(values)
|
||||
return "".join(values)
|
||||
|
||||
|
||||
def all_face_types(n: int, min_outer: int, min_inner: int):
|
||||
for values in itertools.product("OI", repeat=n):
|
||||
face_types = "".join(values)
|
||||
if face_types.count("O") >= min_outer and face_types.count("I") >= min_inner:
|
||||
yield face_types
|
||||
|
||||
|
||||
def best_by(metrics: list[RestrictionMetrics], key_name: str, limit: int):
|
||||
return sorted(metrics, key=lambda m: (getattr(m, key_name), m.n_faces, m.face_types))[
|
||||
:limit
|
||||
]
|
||||
|
||||
|
||||
def print_metric(metric: RestrictionMetrics) -> None:
|
||||
print(f" types={metric.face_types}")
|
||||
print(
|
||||
f" faces={metric.n_faces} outer={metric.n_outer} inner={metric.n_inner} "
|
||||
f"outer_orbits={metric.outer_orbits} inner_orbits={metric.inner_orbits}"
|
||||
)
|
||||
print(
|
||||
f" realized_outer={metric.realized_outer_orbits} "
|
||||
f"realized_inner={metric.realized_inner_orbits} relation={metric.relation_size}"
|
||||
)
|
||||
print(
|
||||
f" blocked_outer={metric.blocked_outer_orbits} "
|
||||
f"({metric.blocked_outer_fraction:.3f}) "
|
||||
f"blocked_inner={metric.blocked_inner_orbits} "
|
||||
f"({metric.blocked_inner_fraction:.3f})"
|
||||
)
|
||||
print(
|
||||
f" min_outer_extensions={metric.min_outer_extensions} "
|
||||
f"avg_outer_extensions={metric.avg_outer_extensions:.3f}"
|
||||
)
|
||||
print(
|
||||
f" density_all={metric.relation_density_all:.6f} "
|
||||
f"density_realized_outer={metric.relation_density_realized_outer:.6f}"
|
||||
)
|
||||
print(f" worst_outer_states={metric.worst_outer_states}")
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
rng = random.Random(args.seed)
|
||||
seen: set[str] = set()
|
||||
metrics: list[RestrictionMetrics] = []
|
||||
|
||||
if args.exhaustive:
|
||||
for n in range(args.min_faces, args.max_faces + 1):
|
||||
for face_types in all_face_types(n, args.min_outer, args.min_inner):
|
||||
metrics.append(analyze_face_types(face_types))
|
||||
else:
|
||||
for _ in range(args.samples):
|
||||
n = rng.randint(args.min_faces, args.max_faces)
|
||||
face_types = random_face_types(rng, n, args.min_outer, args.min_inner)
|
||||
if face_types in seen:
|
||||
continue
|
||||
seen.add(face_types)
|
||||
metrics.append(analyze_face_types(face_types))
|
||||
|
||||
print(f"graphs analyzed: {len(metrics)}")
|
||||
print(
|
||||
"boundary states quotient by color permutations only; "
|
||||
"no rotations or reversals"
|
||||
)
|
||||
print()
|
||||
|
||||
print("most restrictive by min_outer_extensions")
|
||||
for metric in best_by(metrics, "min_outer_extensions", args.limit):
|
||||
print_metric(metric)
|
||||
print()
|
||||
|
||||
print("most restrictive by avg_outer_extensions")
|
||||
for metric in best_by(metrics, "avg_outer_extensions", args.limit):
|
||||
print_metric(metric)
|
||||
print()
|
||||
|
||||
print("most restrictive by relation_density_all")
|
||||
for metric in best_by(metrics, "relation_density_all", args.limit):
|
||||
print_metric(metric)
|
||||
print()
|
||||
|
||||
print("most restrictive by blocked_outer_orbits")
|
||||
for metric in sorted(
|
||||
metrics,
|
||||
key=lambda m: (-m.blocked_outer_orbits, m.n_faces, m.face_types),
|
||||
)[: args.limit]:
|
||||
print_metric(metric)
|
||||
print()
|
||||
|
||||
print("most restrictive by blocked_outer_fraction")
|
||||
for metric in sorted(
|
||||
metrics,
|
||||
key=lambda m: (-m.blocked_outer_fraction, m.n_faces, m.face_types),
|
||||
)[: args.limit]:
|
||||
print_metric(metric)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--min-faces", type=int, default=6)
|
||||
parser.add_argument("--max-faces", type=int, default=10)
|
||||
parser.add_argument("--samples", type=int, default=1000)
|
||||
parser.add_argument("--seed", type=int, default=0)
|
||||
parser.add_argument("--min-outer", type=int, default=1)
|
||||
parser.add_argument("--min-inner", type=int, default=1)
|
||||
parser.add_argument("--limit", type=int, default=5)
|
||||
parser.add_argument("--exhaustive", action="store_true")
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user