Compare commits
41 Commits
94d59ceaed
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f0fdae11d4 | |||
| d9007c8697 | |||
| b1d681f39e | |||
| f54b66f857 | |||
| 5552e07803 | |||
| 163e453464 | |||
| c482bc5633 | |||
| c4339624ce | |||
| d7c93cf2c2 | |||
| 411ff7f465 | |||
| 24a3d89d88 | |||
| bd8499a25b | |||
| 9f6328788c | |||
| 1d981b4d01 | |||
| b70ea2c087 | |||
| d2156f06ee | |||
| 60c9f1d3a8 | |||
| 351ae0cdfe | |||
| c5f81842c7 | |||
| 646cf9d12f | |||
| 251c453437 | |||
| 851ca7fbed | |||
| af60c3b241 | |||
| 4ba9ce47d1 | |||
| 696a6b3104 | |||
| 1e8bee04ce | |||
| 37a7ff0b00 | |||
| 9ef231655e | |||
| d541aea526 | |||
| 2a56322841 | |||
| 51c9efa7f2 | |||
| 464335082d | |||
| 5829938ab0 | |||
| f537db9758 | |||
| 6ef1dc710c | |||
| b605931678 | |||
| 7554582056 | |||
| 192d97a31d | |||
| 4e92dde36e | |||
| 0a3d7b2615 | |||
| 367b5adc71 |
@@ -0,0 +1,103 @@
|
||||
"""Experiment: is a given maximal planar graph in its own flip neighborhood?
|
||||
|
||||
The flip neighborhood N(G) of a maximal planar graph G (Definition
|
||||
"flip-neighborhood" in paper.tex) is the set of maximal planar graphs obtainable
|
||||
from G by a single admissible edge flip: pick an edge uv whose two incident
|
||||
triangular faces are uvw and uvx, delete uv, and insert wx, provided wx is not
|
||||
already an edge.
|
||||
|
||||
We call G a *self-flip-neighbor* iff some admissible flip G^flip(uv) is
|
||||
isomorphic to G --- equivalently, G in N(G). A flip always changes the labelled
|
||||
edge set (it removes uv and adds a non-edge wx), so this is a genuine question
|
||||
about the isomorphism type: it asks whether a single diagonal flip can map the
|
||||
triangulation back onto a copy of itself.
|
||||
|
||||
This module enumerates every admissible flip of G, tests each resulting
|
||||
triangulation for isomorphism to G, and reports the witnessing edges. It can run
|
||||
on a single graph (given as a graph6 string, or the icosahedron by default) or
|
||||
survey every min-degree-5 maximal planar graph over a range of orders.
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))) # repo root for `lib`
|
||||
from typing import Iterator, Any, cast
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
|
||||
# Reuse the flip machinery from the survey in this same directory.
|
||||
from colored_edge_flip_class_survey import face_thirds, canonical_g6 # type: ignore[import]
|
||||
|
||||
|
||||
def admissible_flips(g: Graph) -> Iterator[tuple[Any, Any, Any, Any, Graph]]:
|
||||
"""Yield (u, v, w, x, H) for every admissible edge flip of g: uv is the
|
||||
flipped edge, wx the inserted diagonal (a non-edge), and H = g^flip(uv)."""
|
||||
pairs = face_thirds(g)
|
||||
for u, v in list(g.edges(labels=False)):
|
||||
thirds = pairs.get(frozenset((u, v)))
|
||||
if thirds is None or len(thirds) != 2:
|
||||
continue
|
||||
w, x = thirds
|
||||
if g.has_edge(w, x):
|
||||
continue
|
||||
h = g.copy()
|
||||
h.delete_edge(u, v)
|
||||
h.add_edge(w, x)
|
||||
yield u, v, w, x, h
|
||||
|
||||
|
||||
def self_flip_witnesses(g: Graph) -> list[tuple[Any, Any, Any, Any]]:
|
||||
"""Return every (u, v, w, x) such that the admissible flip uv -> wx yields a
|
||||
graph isomorphic to g. Non-empty iff g lies in its own flip neighborhood."""
|
||||
target = canonical_g6(g)
|
||||
witnesses: list[tuple[Any, Any, Any, Any]] = []
|
||||
for u, v, w, x, h in admissible_flips(g):
|
||||
if canonical_g6(h) == target:
|
||||
witnesses.append((u, v, w, x))
|
||||
return witnesses
|
||||
|
||||
|
||||
def is_self_flip_neighbor(g: Graph) -> bool:
|
||||
"""True iff g in N(g): some admissible flip of g is isomorphic to g."""
|
||||
target = canonical_g6(g)
|
||||
return any(canonical_g6(h) == target for *_e, h in admissible_flips(g))
|
||||
|
||||
|
||||
def report_single(g: Graph) -> bool:
|
||||
"""Print a per-edge flip report for g and return whether it self-flips."""
|
||||
flips = list(admissible_flips(g))
|
||||
witnesses = self_flip_witnesses(g)
|
||||
print(f"graph6={g.graph6_string()} |V|={g.order()} |E|={g.size()} "
|
||||
f"admissible flips={len(flips)} self-flip witnesses={len(witnesses)}")
|
||||
for u, v, w, x in witnesses:
|
||||
print(f" flip edge ({u},{v}) -> ({w},{x}) gives a graph isomorphic to G")
|
||||
print(f" => G {'IS' if witnesses else 'is NOT'} in its own flip neighborhood")
|
||||
return bool(witnesses)
|
||||
|
||||
|
||||
def survey(min_order: int, max_order: int) -> None:
|
||||
"""For each order in [min_order, max_order], count how many min-degree-5
|
||||
maximal planar graphs are self-flip-neighbors."""
|
||||
for n in range(min_order, max_order + 1):
|
||||
gen = graphs.planar_graphs(
|
||||
n, minimum_connectivity=3, maximum_face_size=3, minimum_degree=5
|
||||
)
|
||||
checked = 0
|
||||
self_flip = 0
|
||||
for g in gen:
|
||||
checked += 1
|
||||
if is_self_flip_neighbor(cast(Graph, g)):
|
||||
self_flip += 1
|
||||
print(f"order {n}: {checked} min-degree-5 maximal planar graphs, "
|
||||
f"{self_flip} are self-flip-neighbors")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 2 and sys.argv[1].lstrip("-").isdigit():
|
||||
lo = int(sys.argv[1])
|
||||
hi = int(sys.argv[2]) if len(sys.argv) > 2 else lo
|
||||
survey(lo, hi)
|
||||
else:
|
||||
if len(sys.argv) >= 2:
|
||||
graph = Graph(sys.argv[1])
|
||||
else:
|
||||
graph = graphs.IcosahedralGraph()
|
||||
print("(no graph given; using the icosahedron)")
|
||||
report_single(cast(Graph, graph))
|
||||
@@ -10,12 +10,16 @@
|
||||
\newlabel{lem:edge-deletion-4colorable}{{4.2}{2}}
|
||||
\newlabel{lem:edge-deletion-coloring-structure}{{4.3}{3}}
|
||||
\newlabel{thm:flip-neighborhood-4colorable}{{4.4}{3}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Case\nonbreakingspace 2 of the proof of Theorem\nonbreakingspace 4.4\hbox {}: $u, v$ share color $a$ and $w, x$ share color $c$. The $\{a, b\}$-Kempe path $P$ from $u$ to $v$ separates $w$ from $x$ in the plane, so no $\{c, d\}$-path between $w$ and $x$ can avoid crossing $P$; since the color sets $\{a, b\}$ and $\{c, d\}$ are disjoint, no such path exists.}}{4}{}\protected@file@percent }
|
||||
\newlabel{fig:flip-proof-case-two}{{2}{4}}
|
||||
\newlabel{thm:no-colored-class-contains-G}{{4.5}{4}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{5}{A computational aside: self-flip neighbors}}{4}{}\protected@file@percent }
|
||||
\newlabel{def:self-flip-neighbor}{{5.1}{4}}
|
||||
\newlabel{tocindent-1}{0pt}
|
||||
\newlabel{tocindent0}{0pt}
|
||||
\newlabel{tocindent1}{17.77782pt}
|
||||
\newlabel{tocindent2}{0pt}
|
||||
\newlabel{tocindent3}{0pt}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Case\nonbreakingspace 2 of the proof of Theorem\nonbreakingspace 4.4\hbox {}: $u, v$ share color $a$ and $w, x$ share color $c$. The $\{a, b\}$-Kempe path $P$ from $u$ to $v$ separates $w$ from $x$ in the plane, so no $\{c, d\}$-path between $w$ and $x$ can avoid crossing $P$; since the color sets $\{a, b\}$ and $\{c, d\}$ are disjoint, no such path exists.}}{4}{}\protected@file@percent }
|
||||
\newlabel{fig:flip-proof-case-two}{{2}{4}}
|
||||
\newlabel{thm:no-colored-class-contains-G}{{4.5}{4}}
|
||||
\gdef \@abspage@last{4}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1}{\ignorespaces Self-flip neighbors among min-degree-$5$ maximal planar graphs. No graph on $n \leq 15$ vertices is a self-flip neighbor; beyond the first occurrences the fraction declines steadily.}}{5}{}\protected@file@percent }
|
||||
\newlabel{tab:self-flip}{{1}{5}}
|
||||
\gdef \@abspage@last{5}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Fdb version 3
|
||||
["pdflatex"] 1778743331 "paper.tex" "paper.pdf" "paper" 1778743331
|
||||
["pdflatex"] 1781844089 "paper.tex" "paper.pdf" "paper" 1781844090
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
|
||||
@@ -131,8 +131,8 @@
|
||||
"/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 ""
|
||||
"/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
|
||||
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
|
||||
"paper.aux" 1778743331 1709 057e58fcb5472314b0a7029f2c0f7505 "pdflatex"
|
||||
"paper.tex" 1778743323 14730 0431b5dd1f68c135b8365d9286869b8f ""
|
||||
"paper.aux" 1781844090 2204 9c1b0b970c8aeef1caf450ea82c0c00d "pdflatex"
|
||||
"paper.tex" 1781844079 17938 6757143b0e59891047a9dd2db3f626cf ""
|
||||
(generated)
|
||||
"paper.aux"
|
||||
"paper.log"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 MAY 2026 03:22
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 19 JUN 2026 00:41
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
@@ -486,39 +486,43 @@ File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
|
||||
e
|
||||
))
|
||||
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
|
||||
[2] [3] [4] (./paper.aux) )
|
||||
[2] [3]
|
||||
|
||||
LaTeX Warning: `h' float specifier changed to `ht'.
|
||||
|
||||
[4] [5] (./paper.aux) )
|
||||
Here is how much of TeX's memory you used:
|
||||
13206 strings out of 478268
|
||||
266409 string characters out of 5846347
|
||||
540812 words of memory out of 5000000
|
||||
31041 multiletter control sequences out of 15000+600000
|
||||
13208 strings out of 478268
|
||||
266448 string characters out of 5846347
|
||||
541829 words of memory out of 5000000
|
||||
31043 multiletter control sequences out of 15000+600000
|
||||
477211 words of font info for 59 fonts, out of 8000000 for 9000
|
||||
1302 hyphenation exceptions out of 8191
|
||||
100i,9n,104p,495b,794s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf-dist/fonts/type1/publ
|
||||
ic/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
|
||||
c/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
|
||||
c/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
|
||||
/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/
|
||||
amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
|
||||
onts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
|
||||
ts/cm/cmmi9.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
|
||||
/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
|
||||
m/cmr6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/c
|
||||
mr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8
|
||||
.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr9.pf
|
||||
b></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb><
|
||||
/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb></u
|
||||
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy9.pfb></usr
|
||||
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/
|
||||
local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/lo
|
||||
cal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
|
||||
Output written on paper.pdf (4 pages, 246274 bytes).
|
||||
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
|
||||
msfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
|
||||
fonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
|
||||
onts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
|
||||
ts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
|
||||
/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
|
||||
m/cmmi9.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
|
||||
cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
|
||||
r6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.
|
||||
pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr9.pfb></
|
||||
usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></u
|
||||
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr
|
||||
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb></usr/l
|
||||
ocal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy9.pfb></usr/loc
|
||||
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/loca
|
||||
l/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/
|
||||
texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
|
||||
Output written on paper.pdf (5 pages, 251916 bytes).
|
||||
PDF statistics:
|
||||
120 PDF objects out of 1000 (max. 8388607)
|
||||
73 compressed objects within 1 object stream
|
||||
123 PDF objects out of 1000 (max. 8388607)
|
||||
75 compressed objects within 1 object stream
|
||||
0 named destinations out of 1000 (max. 500000)
|
||||
13 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
|
||||
@@ -371,6 +371,79 @@ in particular, $\varphi$ is a proper $4$-coloring of $H_k = G$. But
|
||||
$\chi(G) \geq 5$ admits no such coloring, a contradiction.
|
||||
\end{proof}
|
||||
|
||||
\section{A computational aside: self-flip neighbors}
|
||||
|
||||
Since $\mathcal{N}(G)$ is defined up to isomorphism, it is natural to
|
||||
ask whether $G$ can be one of its own flip neighbors.
|
||||
|
||||
\begin{definition}[Self-flip neighbor]\label{def:self-flip-neighbor}
|
||||
A maximal planar graph $G$ is a \emph{self-flip neighbor} if
|
||||
$G \in \mathcal{N}(G)$; that is, if some admissible edge flip
|
||||
$G^{\mathrm{flip}(uv)}$ is isomorphic to $G$.
|
||||
\end{definition}
|
||||
|
||||
A single flip always changes the labelled edge set --- it deletes
|
||||
$uv$ and inserts a non-edge $wx$ --- so the property is genuinely one
|
||||
of the isomorphism type: it asks whether a diagonal flip can carry the
|
||||
triangulation back onto a copy of itself. Equivalently, the degree
|
||||
changes induced by the flip ($-1$ at $u$ and $v$, $+1$ at $w$ and $x$)
|
||||
must be undone by an automorphism of the resulting graph.
|
||||
|
||||
We surveyed every maximal planar graph of minimum degree $5$ on
|
||||
$n \leq 25$ vertices, counting those that are self-flip neighbors.
|
||||
The results are recorded in Table~\ref{tab:self-flip}.
|
||||
|
||||
\begin{table}[h]
|
||||
\centering
|
||||
\begin{tabular}{rrrr}
|
||||
\hline
|
||||
$n$ & min-degree-$5$ & self-flip & fraction \\
|
||||
& triangulations & neighbors & \\
|
||||
\hline
|
||||
$12$ & $1$ & $0$ & $0\%$ \\
|
||||
$13$ & $0$ & $0$ & --- \\
|
||||
$14$ & $1$ & $0$ & $0\%$ \\
|
||||
$15$ & $1$ & $0$ & $0\%$ \\
|
||||
$16$ & $3$ & $1$ & $33.3\%$ \\
|
||||
$17$ & $4$ & $1$ & $25.0\%$ \\
|
||||
$18$ & $12$ & $2$ & $16.7\%$ \\
|
||||
$19$ & $23$ & $5$ & $21.7\%$ \\
|
||||
$20$ & $73$ & $12$ & $16.4\%$ \\
|
||||
$21$ & $192$ & $27$ & $14.1\%$ \\
|
||||
$22$ & $651$ & $51$ & $7.8\%$ \\
|
||||
$23$ & $2070$ & $120$ & $5.8\%$ \\
|
||||
$24$ & $7290$ & $273$ & $3.7\%$ \\
|
||||
$25$ & $25381$ & $598$ & $2.4\%$ \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\caption{Self-flip neighbors among min-degree-$5$ maximal planar
|
||||
graphs. No graph on $n \leq 15$ vertices is a self-flip neighbor;
|
||||
beyond the first occurrences the fraction declines steadily.}
|
||||
\label{tab:self-flip}
|
||||
\end{table}
|
||||
|
||||
\begin{remark}
|
||||
The proportion of self-flip neighbors declines as $n$ grows --- from a
|
||||
peak near a third at $n = 16$ to roughly $2\%$ at $n = 25$ --- and the
|
||||
trend is the one to expect. A self-flip neighbor requires a flip whose
|
||||
induced degree changes are reversed by an automorphism of the flipped
|
||||
graph, but min-degree-$5$ triangulations are asymptotically rigid: the
|
||||
share with a nontrivial automorphism group, let alone one of the
|
||||
required form, tends to $0$. The absolute count of self-flip neighbors
|
||||
continues to grow, but ever more slowly than the census itself, so the
|
||||
fraction appears to vanish in the limit.
|
||||
|
||||
This observation is not, by itself, useful for narrowing down the
|
||||
minimal criminal $G_0$. Whether or not $G_0$ is a self-flip neighbor,
|
||||
Theorem~\ref{thm:flip-neighborhood-4colorable} already shows every
|
||||
graph in $\mathcal{N}(G_0)$ is $4$-colorable; self-flip neighborness
|
||||
is a generic structural feature of the surrounding census rather than a
|
||||
property forced on, or forbidden of, a minimum counterexample. It
|
||||
neither includes nor excludes any candidate, and so contributes nothing
|
||||
to the elimination program beyond confirming that the flip operation
|
||||
behaves on min-degree-$5$ triangulations as one would anticipate.
|
||||
\end{remark}
|
||||
|
||||
\end{document}
|
||||
|
||||
%-----------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Survey: for each n, how many maximal planar graphs (plane-triangulation
|
||||
iso classes) are *bridge-derived* level graphs of some Even Level Graph.
|
||||
|
||||
Bridge-derivedness is decided exhaustively via the backward bridge-switch
|
||||
orbit (see small_n_probe.is_bridge_derived): a triangulation G is
|
||||
bridge-derived iff some valid parity partition L of G admits an Even Level
|
||||
Graph (parity L) in G's backward bridge-orbit. Feasible only at small n.
|
||||
|
||||
Also cross-tabulates against the intertwining-tree property so the two
|
||||
covering families in the disjunction conjecture can be compared.
|
||||
|
||||
Usage: python3 bridge_derived_survey.py [n_max] (default 11)
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from small_n_probe import is_bridge_derived
|
||||
from test_disjunction import is_intertwining_tree
|
||||
|
||||
|
||||
def survey_n(n):
|
||||
t0 = time.time()
|
||||
tris = enumerate_all_triangulations(n)
|
||||
n_bridge = 0
|
||||
n_inter = 0
|
||||
n_bridge_and_inter = 0
|
||||
n_bridge_only = 0
|
||||
n_inter_only = 0
|
||||
n_neither = 0
|
||||
for G in tris:
|
||||
bd = is_bridge_derived(G)
|
||||
it = is_intertwining_tree(G)[0]
|
||||
if bd:
|
||||
n_bridge += 1
|
||||
if it:
|
||||
n_inter += 1
|
||||
if bd and it:
|
||||
n_bridge_and_inter += 1
|
||||
elif bd:
|
||||
n_bridge_only += 1
|
||||
elif it:
|
||||
n_inter_only += 1
|
||||
else:
|
||||
n_neither += 1
|
||||
return {
|
||||
'n': n,
|
||||
'total': len(tris),
|
||||
'bridge': n_bridge,
|
||||
'inter': n_inter,
|
||||
'bridge_and_inter': n_bridge_and_inter,
|
||||
'bridge_only': n_bridge_only,
|
||||
'inter_only': n_inter_only,
|
||||
'neither': n_neither,
|
||||
'elapsed': time.time() - t0,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
n_max = int(sys.argv[1]) if len(sys.argv) > 1 else 11
|
||||
rows = []
|
||||
print(f'{"n":>3} {"total":>7} {"bridge-deriv":>13} {"%":>6} '
|
||||
f'{"inter":>7} {"b&i":>6} {"b-only":>7} {"i-only":>7} '
|
||||
f'{"neither":>8} {"sec":>7}', flush=True)
|
||||
for n in range(6, n_max + 1):
|
||||
r = survey_n(n)
|
||||
rows.append(r)
|
||||
pct = 100.0 * r['bridge'] / r['total'] if r['total'] else 0.0
|
||||
print(f'{r["n"]:>3} {r["total"]:>7} {r["bridge"]:>13} {pct:>5.1f}% '
|
||||
f'{r["inter"]:>7} {r["bridge_and_inter"]:>6} '
|
||||
f'{r["bridge_only"]:>7} {r["inter_only"]:>7} '
|
||||
f'{r["neither"]:>8} {r["elapsed"]:>6.1f}', flush=True)
|
||||
if r['neither']:
|
||||
print(f' *** {r["neither"]} triangulation(s) at n={n} are '
|
||||
f'NEITHER bridge-derived nor intertwining trees ***',
|
||||
flush=True)
|
||||
return rows
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Draw the two former "failures" (n=9 bipyramid, n=10 bipyramid+stacked) the
|
||||
RIGHT way: a proper 4-colouring whose 4 colours split into two complementary
|
||||
pairs, each inducing an outerplanar (bipartite => even-cycle) subgraph.
|
||||
|
||||
Top row: the planar drawing with the 4-colouring; edges of the two split
|
||||
classes drawn in two styles. Bottom row: the two complementary subgraphs shown
|
||||
separately, each annotated outerplanar (tree / forest / even cycle).
|
||||
|
||||
Renders into this experiments folder.
|
||||
"""
|
||||
import os
|
||||
from itertools import combinations
|
||||
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
T24 = [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8),
|
||||
(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8),
|
||||
(2, 3), (2, 4), (3, 5), (4, 8), (5, 6), (6, 7), (7, 8)]
|
||||
T94 = [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9),
|
||||
(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8),
|
||||
(2, 3), (2, 4), (3, 5), (4, 8), (5, 6), (6, 7), (7, 8),
|
||||
(7, 9), (8, 9)]
|
||||
|
||||
CMAP = {0: '#444444', 1: '#d62728', 2: '#1f77b4', 3: '#2ca02c'}
|
||||
CNAME = {0: 'grey', 1: 'red', 2: 'blue', 3: 'green'}
|
||||
SPLITS = [({0, 1}, {2, 3}), ({0, 2}, {1, 3}), ({0, 3}, {1, 2})]
|
||||
|
||||
|
||||
def is_outerplanar(G):
|
||||
if G.number_of_nodes() <= 3:
|
||||
return True
|
||||
H = G.copy()
|
||||
apex = max(H.nodes()) + 1
|
||||
for v in G.nodes():
|
||||
H.add_edge(apex, v)
|
||||
return nx.check_planarity(H)[0]
|
||||
|
||||
|
||||
def enumerate_4colorings(G):
|
||||
nodes = list(G.nodes())
|
||||
adj = {v: set(G.neighbors(v)) for v in nodes}
|
||||
coloring = {}
|
||||
|
||||
def bt(i, mx):
|
||||
if i == len(nodes):
|
||||
yield dict(coloring)
|
||||
return
|
||||
v = nodes[i]
|
||||
used = {coloring[w] for w in adj[v] if w in coloring}
|
||||
for c in range(min(3, mx + 1) + 1):
|
||||
if c in used:
|
||||
continue
|
||||
coloring[v] = c
|
||||
yield from bt(i + 1, max(mx, c))
|
||||
del coloring[v]
|
||||
|
||||
yield from bt(0, -1)
|
||||
|
||||
|
||||
def find_good_split(G):
|
||||
for col in enumerate_4colorings(G):
|
||||
for A, B in SPLITS:
|
||||
va = [v for v in G if col[v] in A]
|
||||
vb = [v for v in G if col[v] in B]
|
||||
GA, GB = G.subgraph(va), G.subgraph(vb)
|
||||
if (is_outerplanar(GA) and nx.is_bipartite(GA)
|
||||
and is_outerplanar(GB) and nx.is_bipartite(GB)):
|
||||
return col, (A, B)
|
||||
return None, None
|
||||
|
||||
|
||||
def bipyramid_pos(rim_cycle, apexA, apexB):
|
||||
k = len(rim_cycle)
|
||||
order = rim_cycle[1:] + [rim_cycle[0]]
|
||||
pos = {}
|
||||
for i, v in enumerate(order):
|
||||
x = 1.7 * (1 - 2 * i / (k - 1))
|
||||
y = 1.0 * (x / 1.7) ** 2
|
||||
pos[v] = (x, y)
|
||||
pos[apexA] = (0.0, 0.62)
|
||||
pos[apexB] = (0.0, -1.7)
|
||||
return pos
|
||||
|
||||
|
||||
def describe(G):
|
||||
if G.number_of_edges() == 0:
|
||||
return "edgeless (isolated vertices)"
|
||||
if nx.is_forest(G):
|
||||
return "forest (tree, no cycles)"
|
||||
girth_even = all(len(c) % 2 == 0 for c in nx.cycle_basis(G))
|
||||
comps = nx.number_connected_components(G)
|
||||
tag = "even cycles" if girth_even else "ODD CYCLE!"
|
||||
return f"outerplanar, {tag}, {comps} component(s)"
|
||||
|
||||
|
||||
def draw_case(fig, gs_row, edges, pos, title):
|
||||
G = nx.Graph(edges)
|
||||
col, split = find_good_split(G)
|
||||
A, B = split
|
||||
node_colors = [CMAP[col[v]] for v in G.nodes()]
|
||||
ea = [e for e in G.edges() if col[e[0]] in A and col[e[1]] in A]
|
||||
eb = [e for e in G.edges() if col[e[0]] in B and col[e[1]] in B]
|
||||
ecross = [e for e in G.edges() if e not in ea and e not in eb]
|
||||
|
||||
# left: whole graph, both classes highlighted
|
||||
ax = fig.add_subplot(gs_row[0])
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=ecross,
|
||||
edge_color='#dddddd', width=1.0)
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=ea,
|
||||
edge_color='#e8860a', width=3.0)
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edgelist=eb,
|
||||
edge_color='#7b2fbf', width=3.0, style='dashed')
|
||||
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=720,
|
||||
edgecolors='black', linewidths=1.4, ax=ax)
|
||||
nx.draw_networkx_labels(G, pos, ax=ax, font_color='white',
|
||||
font_weight='bold', font_size=11)
|
||||
an = "/".join(CNAME[c] for c in sorted(A))
|
||||
bn = "/".join(CNAME[c] for c in sorted(B))
|
||||
ax.set_title(f"{title}\nsplit [{an}] (orange solid) | "
|
||||
f"[{bn}] (purple dashed)", fontsize=10)
|
||||
ax.axis('off'); ax.set_aspect('equal')
|
||||
|
||||
# middle: subgraph A alone
|
||||
GA = G.subgraph([v for v in G if col[v] in A])
|
||||
axA = fig.add_subplot(gs_row[1])
|
||||
nx.draw_networkx_edges(GA, pos, ax=axA, edge_color='#e8860a', width=3.0)
|
||||
nx.draw_networkx_nodes(GA, pos, node_color=[CMAP[col[v]] for v in GA],
|
||||
node_size=720, edgecolors='black',
|
||||
linewidths=1.4, ax=axA)
|
||||
nx.draw_networkx_labels(GA, pos, ax=axA, font_color='white',
|
||||
font_weight='bold', font_size=11)
|
||||
axA.set_title(f"[{an}] subgraph\n{describe(GA)}", fontsize=9)
|
||||
axA.axis('off'); axA.set_aspect('equal')
|
||||
|
||||
# right: subgraph B alone
|
||||
GB = G.subgraph([v for v in G if col[v] in B])
|
||||
axB = fig.add_subplot(gs_row[2])
|
||||
nx.draw_networkx_edges(GB, pos, ax=axB, edge_color='#7b2fbf', width=3.0)
|
||||
nx.draw_networkx_nodes(GB, pos, node_color=[CMAP[col[v]] for v in GB],
|
||||
node_size=720, edgecolors='black',
|
||||
linewidths=1.4, ax=axB)
|
||||
nx.draw_networkx_labels(GB, pos, ax=axB, font_color='white',
|
||||
font_weight='bold', font_size=11)
|
||||
axB.set_title(f"[{bn}] subgraph\n{describe(GB)}", fontsize=9)
|
||||
axB.axis('off'); axB.set_aspect('equal')
|
||||
|
||||
|
||||
rim = [2, 3, 5, 6, 7, 8, 4]
|
||||
pos24 = bipyramid_pos(rim, 0, 1)
|
||||
pos94 = dict(pos24)
|
||||
pos94[9] = ((pos24[0][0] + pos24[7][0] + pos24[8][0]) / 3,
|
||||
(pos24[0][1] + pos24[7][1] + pos24[8][1]) / 3)
|
||||
|
||||
fig = plt.figure(figsize=(15, 10))
|
||||
gs = fig.add_gridspec(2, 3)
|
||||
draw_case(fig, [gs[0, 0], gs[0, 1], gs[0, 2]], T24, pos24,
|
||||
"n=9 T24: 7-gonal bipyramid")
|
||||
draw_case(fig, [gs[1, 0], gs[1, 1], gs[1, 2]], T94, pos94,
|
||||
"n=10 T94: bipyramid + stacked vertex 9")
|
||||
fig.suptitle("Both decompose: 4-colouring -> 2+2 colour split -> two "
|
||||
"complementary outerplanar even-cycle subgraphs", fontsize=12)
|
||||
fig.tight_layout(rect=[0, 0, 1, 0.97])
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'split_decomposition.png')
|
||||
fig.savefig(out, dpi=140)
|
||||
print(f"wrote {out}")
|
||||
|
After Width: | Height: | Size: 255 KiB |
@@ -0,0 +1,125 @@
|
||||
"""Corrected sanity check for the nested-outerplanar-shells construction.
|
||||
|
||||
The construction splits the FOUR colours into TWO complementary pairs (two
|
||||
parity classes). Each pair induces a bipartite subgraph; the two subgraphs
|
||||
partition the vertex set. For a nested-shell decomposition we want BOTH
|
||||
complementary subgraphs to be outerplanar (bipartite => only even cycles, so
|
||||
"even cycles" is automatic; outerplanar is the binding condition).
|
||||
|
||||
There are exactly 3 ways to split {0,1,2,3} into two pairs:
|
||||
{0,1}|{2,3}, {0,2}|{1,3}, {0,3}|{1,2}.
|
||||
|
||||
Criterion (per triangulation): does SOME proper 4-colouring admit SOME split
|
||||
whose two complementary subgraphs are both outerplanar?
|
||||
|
||||
This is the right test (an earlier version wrongly demanded that all SIX
|
||||
colour pairs be outerplanar, which odd bipyramids fail on the apex/heavy-rim
|
||||
pair -- but that pair is never one we'd use).
|
||||
|
||||
Usage: python3 two_color_split_survey.py [n_max] (default 10)
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
import networkx as nx
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
# the 3 ways to split 4 colours into two complementary pairs
|
||||
SPLITS = [(({0, 1}), ({2, 3})),
|
||||
(({0, 2}), ({1, 3})),
|
||||
(({0, 3}), ({1, 2}))]
|
||||
|
||||
|
||||
def is_outerplanar(G):
|
||||
if G.number_of_nodes() <= 3:
|
||||
return True
|
||||
H = G.copy()
|
||||
apex = max(H.nodes()) + 1
|
||||
for v in G.nodes():
|
||||
H.add_edge(apex, v)
|
||||
return nx.check_planarity(H)[0]
|
||||
|
||||
|
||||
def enumerate_4colorings(G):
|
||||
nodes = list(G.nodes())
|
||||
adj = {v: set(G.neighbors(v)) for v in nodes}
|
||||
coloring = {}
|
||||
|
||||
def bt(i, mx):
|
||||
if i == len(nodes):
|
||||
yield dict(coloring)
|
||||
return
|
||||
v = nodes[i]
|
||||
used = {coloring[w] for w in adj[v] if w in coloring}
|
||||
for c in range(min(3, mx + 1) + 1):
|
||||
if c in used:
|
||||
continue
|
||||
coloring[v] = c
|
||||
yield from bt(i + 1, max(mx, c))
|
||||
del coloring[v]
|
||||
|
||||
yield from bt(0, -1)
|
||||
|
||||
|
||||
def outerplanar_even(G):
|
||||
"""Outerplanar AND every cycle even (i.e. bipartite). Disconnected ok.
|
||||
For a two-colour subgraph of a proper colouring bipartiteness is automatic,
|
||||
but we verify it explicitly so the criterion is honestly enforced."""
|
||||
return is_outerplanar(G) and nx.is_bipartite(G)
|
||||
|
||||
|
||||
def good_split(G, col):
|
||||
"""Return the first (sideA, sideB) split whose two complementary
|
||||
subgraphs are both outerplanar-with-even-cycles, or None.
|
||||
Disconnected subgraphs are allowed."""
|
||||
for A, B in SPLITS:
|
||||
va = [v for v in G if col[v] in A]
|
||||
vb = [v for v in G if col[v] in B]
|
||||
if outerplanar_even(G.subgraph(va)) and outerplanar_even(G.subgraph(vb)):
|
||||
return (A, B)
|
||||
return None
|
||||
|
||||
|
||||
def has_good_coloring(G):
|
||||
"""True iff SOME proper 4-colouring admits a valid 2+2 split.
|
||||
Early-exits on the first good colouring."""
|
||||
for col in enumerate_4colorings(G):
|
||||
if good_split(G, col) is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def survey_n(n):
|
||||
t0 = time.time()
|
||||
tris = enumerate_all_triangulations(n)
|
||||
n_good = 0
|
||||
bad = []
|
||||
for gi, G in enumerate(tris):
|
||||
ok = has_good_coloring(G)
|
||||
if ok:
|
||||
n_good += 1
|
||||
else:
|
||||
bad.append((gi, G))
|
||||
return n, len(tris), n_good, bad, time.time() - t0
|
||||
|
||||
|
||||
def main():
|
||||
n_max = int(sys.argv[1]) if len(sys.argv) > 1 else 10
|
||||
print(f"{'n':>3} {'tris':>6} {'has 2+2 split':>14} {'time(s)':>8}")
|
||||
print("-" * 38)
|
||||
for n in range(6, n_max + 1):
|
||||
n, ntri, ngood, bad, dt = survey_n(n)
|
||||
flag = "" if ngood == ntri else " <-- GAP"
|
||||
print(f"{n:>3} {ntri:>6} {ngood:>9}/{ntri:<4} {dt:>8.1f}{flag}")
|
||||
for gi, G in bad[:5]:
|
||||
edges = sorted(tuple(sorted(e)) for e in G.edges())
|
||||
print(f" no split: T{gi} edges={edges}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -44,19 +44,21 @@
|
||||
\newlabel{def:intertwining-tree}{{4.6}{7}{Intertwining tree}{theorem.4.6}{}}
|
||||
\newlabel{thm:intertwining-iff-hamiltonian-dual}{{4.7}{7}{}{theorem.4.7}{}}
|
||||
\newlabel{conj:every-triangulation-derived}{{4.8}{7}{}{theorem.4.8}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{The boundary case $n = 21$}}{7}{section*.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {2}{\ignorespaces Bridge-derived census for $6 \leq n \leq 10$. \emph {bridge-derived} counts plane-triangulation iso classes that are bridge-derived level graphs of some Even Level Graph, decided by exhaustive backward bridge-switch search over all valid parity partitions; \% is its fraction of all triangulations. \emph {intertwining only} counts those that are intertwining trees but not bridge-derived; \emph {neither} counts those covered by no disjunct. Every triangulation in this range is an intertwining tree, and every bridge-derived one is too, so bridge-derived $\subseteq $ intertwining tree here.}}{8}{table.2}\protected@file@percent }
|
||||
\newlabel{tab:bridge-census}{{2}{8}{Bridge-derived census for $6 \leq n \leq 10$. \emph {bridge-derived} counts plane-triangulation iso classes that are bridge-derived level graphs of some Even Level Graph, decided by exhaustive backward bridge-switch search over all valid parity partitions; \% is its fraction of all triangulations. \emph {intertwining only} counts those that are intertwining trees but not bridge-derived; \emph {neither} counts those covered by no disjunct. Every triangulation in this range is an intertwining tree, and every bridge-derived one is too, so bridge-derived $\subseteq $ intertwining tree here}{table.2}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{The boundary case $n = 21$}}{8}{section*.2}\protected@file@percent }
|
||||
\citation{holton-mckay}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {2}{\ignorespaces The six Holton--McKay duals at $n = 21$, the first triangulations that are not intertwining trees. Each is a bridge-derived level graph: duals $1$ and $2$ are Even Level Graphs outright (zero switches), and the remaining four reach an Even Level Graph in $1$--$4$ bridge switches. All witnesses are step-verified.}}{8}{table.2}\protected@file@percent }
|
||||
\newlabel{tab:n21}{{2}{8}{The six Holton--McKay duals at $n = 21$, the first triangulations that are not intertwining trees. Each is a bridge-derived level graph: duals $1$ and $2$ are Even Level Graphs outright (zero switches), and the remaining four reach an Even Level Graph in $1$--$4$ bridge switches. All witnesses are step-verified}{table.2}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{The cyclically-$5$-connected case: $n = 24$}}{8}{section*.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3}{\ignorespaces The six Holton--McKay duals at $n = 21$, the first triangulations that are not intertwining trees. Each is a bridge-derived level graph: duals $1$ and $2$ are Even Level Graphs outright (zero switches), and the remaining four reach an Even Level Graph in $1$--$4$ bridge switches. All witnesses are step-verified.}}{9}{table.3}\protected@file@percent }
|
||||
\newlabel{tab:n21}{{3}{9}{The six Holton--McKay duals at $n = 21$, the first triangulations that are not intertwining trees. Each is a bridge-derived level graph: duals $1$ and $2$ are Even Level Graphs outright (zero switches), and the remaining four reach an Even Level Graph in $1$--$4$ bridge switches. All witnesses are step-verified}{table.3}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {5}{\ignorespaces The six Holton--McKay duals, drawn as crossing-free planar graphs and coloured by parity (blue even, orange odd, with respect to the fixed level-parity labelling). The solid green edges are the bridge edges introduced by the bridge switches from each dual's witness Even Level Graph. Each green edge is a bridge of its parity subgraph, so no new cycle -- and in particular no odd cycle -- is created; duals $1$ and $2$ coincide with their Even Level Graphs and have no added edge.}}{9}{figure.5}\protected@file@percent }
|
||||
\newlabel{fig:n21-duals}{{5}{9}{The six Holton--McKay duals, drawn as crossing-free planar graphs and coloured by parity (blue even, orange odd, with respect to the fixed level-parity labelling). The solid green edges are the bridge edges introduced by the bridge switches from each dual's witness Even Level Graph. Each green edge is a bridge of its parity subgraph, so no new cycle -- and in particular no odd cycle -- is created; duals $1$ and $2$ coincide with their Even Level Graphs and have no added edge}{figure.5}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {6}{\ignorespaces The $24$-vertex dual $T$ of the unique $44$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph (Holton--McKay Fig.\nonbreakingspace 2.10), drawn crossing-free and coloured by the fixed parity labelling (blue even, orange odd). $T$ is $5$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{6,19\}$ and $\{20,22\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $19$) to $T$. Each green edge is a bridge of its parity subgraph -- $\{6, 19\}$ in the even subgraph, $\{20,22\}$ in the odd -- so no new cycle, and in particular no odd cycle, is created.}}{10}{figure.6}\protected@file@percent }
|
||||
\newlabel{fig:n24-dual}{{6}{10}{The $24$-vertex dual $T$ of the unique $44$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph (Holton--McKay Fig.~2.10), drawn crossing-free and coloured by the fixed parity labelling (blue even, orange odd). $T$ is $5$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{6,19\}$ and $\{20,22\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $19$) to $T$. Each green edge is a bridge of its parity subgraph -- $\{6, 19\}$ in the even subgraph, $\{20,22\}$ in the odd -- so no new cycle, and in particular no odd cycle, is created}{figure.6}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{The cyclically-$5$-connected case: $n = 24$}}{10}{section*.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Beyond $n = 24$: enumeration and the next $5$-connected core}}{10}{section*.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Toward a characterization of bridge-derived graphs}}{11}{section*.5}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {6}{\ignorespaces The $24$-vertex dual $T$ of the unique $44$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph (Holton--McKay Fig.\nonbreakingspace 2.10), drawn crossing-free and coloured by the fixed parity labelling (blue even, orange odd). $T$ is $5$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{6,19\}$ and $\{20,22\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $19$) to $T$. Each green edge is a bridge of its parity subgraph -- $\{6, 19\}$ in the even subgraph, $\{20,22\}$ in the odd -- so no new cycle, and in particular no odd cycle, is created.}}{11}{figure.6}\protected@file@percent }
|
||||
\newlabel{fig:n24-dual}{{6}{11}{The $24$-vertex dual $T$ of the unique $44$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph (Holton--McKay Fig.~2.10), drawn crossing-free and coloured by the fixed parity labelling (blue even, orange odd). $T$ is $5$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{6,19\}$ and $\{20,22\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $19$) to $T$. Each green edge is a bridge of its parity subgraph -- $\{6, 19\}$ in the even subgraph, $\{20,22\}$ in the odd -- so no new cycle, and in particular no odd cycle, is created}{figure.6}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7}{\ignorespaces The $25$-vertex dual $T_{25}$ of the unique $46$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph -- the only such cubic graph at $46$ vertices and the second internally $6$-connected core known. Drawn crossing-free and coloured by parity (blue even, orange odd) for its witness partition. $T_{25}$ is internally $6$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{1,6\}$ and $\{22,24\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $24$) to $T_{25}$. Each is a bridge of the even parity subgraph.}}{12}{figure.7}\protected@file@percent }
|
||||
\newlabel{fig:n25-dual}{{7}{12}{The $25$-vertex dual $T_{25}$ of the unique $46$-vertex non-Hamiltonian cyclically $5$-connected cubic planar graph -- the only such cubic graph at $46$ vertices and the second internally $6$-connected core known. Drawn crossing-free and coloured by parity (blue even, orange odd) for its witness partition. $T_{25}$ is internally $6$-connected and not an intertwining tree, yet is a bridge-derived level graph: the two solid green edges $\{1,6\}$ and $\{22,24\}$ are the bridge edges introduced by the two bridge switches carrying its witness Even Level Graph (source $24$) to $T_{25}$. Each is a bridge of the even parity subgraph}{figure.7}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Toward a characterization of bridge-derived graphs}}{12}{section*.5}\protected@file@percent }
|
||||
\bibcite{holton-mckay}{1}
|
||||
\newlabel{tocindent-1}{0pt}
|
||||
\newlabel{tocindent0}{14.69437pt}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Fdb version 3
|
||||
["pdflatex"] 1779469001 "/Users/didericis/Code/math-research/papers/even_level_graph_generators/paper.tex" "paper.pdf" "paper" 1779469003
|
||||
"/Users/didericis/Code/math-research/papers/even_level_graph_generators/paper.tex" 1779468999 23834 39061385c4cc2522155026d2f8574bbd ""
|
||||
["pdflatex"] 1781848805 "paper.tex" "paper.pdf" "paper" 1781848808
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
|
||||
@@ -20,6 +19,7 @@
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm" 1136768653 768 1321e9409b4137d6fb428ac9dc956269 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb" 1248133631 30251 6afa5cb1d0204815a708a080681d4674 ""
|
||||
@@ -33,6 +33,7 @@
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb" 1248133631 31099 c85edf1dd5b9e826d67c9c7293b6786c ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb" 1248133631 31764 459c573c03a4949a528c2cc7f557e217 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1461363279 71627 94eb9990bed73c364d7f53f960cc8c5b ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/generic/atbegshi/atbegshi.sty" 1575674566 24708 5584a51a7101caf7e6bbf1fc27d8f7b1 ""
|
||||
@@ -91,10 +92,12 @@
|
||||
"fig_level_cycle.png" 1779389598 83736 ee54074ab1383a0dcc7fc20387e34bdc ""
|
||||
"fig_levels.png" 1779389598 88029 5564f46c0a183f3777727b651e7dc461 ""
|
||||
"fig_parity_subgraph.png" 1779389598 191771 f069aa94c8f49b3c7fd9c71426feff2d ""
|
||||
"figures/core_n25_dual.png" 1779491939 167150 1ff2a9ce9f23b303c20e8a8910b41205 ""
|
||||
"figures/fig210_dual.png" 1779469439 152438 ac3c4fe29042435cab15ea90ee80b805 ""
|
||||
"figures/n21_duals.png" 1779463364 667947 fd52170c20399b0c2dff901831fad5d5 ""
|
||||
"paper.aux" 1779469003 8486 a43934b41579f5535915f5341c4d1db7 "pdflatex"
|
||||
"paper.out" 1779469003 1088 cf07a31709ba02be3ba2bc89322768d0 "pdflatex"
|
||||
"paper.tex" 1779468999 23834 39061385c4cc2522155026d2f8574bbd ""
|
||||
"paper.aux" 1781848808 13255 0b6591b567d7fefa0f2a1ac1716e57fc "pdflatex"
|
||||
"paper.out" 1781848808 2030 d310c1d6d9f73494fc676a3dd19e31e8 "pdflatex"
|
||||
"paper.tex" 1781848188 38745 a8bea15e6bfeb354af5c8a7ce030fc53 ""
|
||||
(generated)
|
||||
"paper.aux"
|
||||
"paper.log"
|
||||
|
||||
@@ -2,7 +2,7 @@ PWD /Users/didericis/Code/math-research/papers/even_level_graph_generators
|
||||
INPUT /usr/local/texlive/2022/texmf.cnf
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/web2c/texmf.cnf
|
||||
INPUT /usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt
|
||||
INPUT /Users/didericis/Code/math-research/papers/even_level_graph_generators/paper.tex
|
||||
INPUT paper.tex
|
||||
OUTPUT paper.log
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
|
||||
@@ -571,6 +571,17 @@ INPUT ./figures/n21_duals.png
|
||||
INPUT figures/n21_duals.png
|
||||
INPUT ./figures/n21_duals.png
|
||||
INPUT ./figures/n21_duals.png
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm
|
||||
INPUT ./figures/fig210_dual.png
|
||||
INPUT ./figures/fig210_dual.png
|
||||
INPUT figures/fig210_dual.png
|
||||
INPUT ./figures/fig210_dual.png
|
||||
INPUT ./figures/fig210_dual.png
|
||||
INPUT ./figures/core_n25_dual.png
|
||||
INPUT ./figures/core_n25_dual.png
|
||||
INPUT figures/core_n25_dual.png
|
||||
INPUT ./figures/core_n25_dual.png
|
||||
INPUT ./figures/core_n25_dual.png
|
||||
INPUT paper.aux
|
||||
INPUT ./paper.out
|
||||
INPUT ./paper.out
|
||||
@@ -587,4 +598,5 @@ INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.p
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb
|
||||
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 22 MAY 2026 20:05
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 19 JUN 2026 02:00
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
@@ -409,86 +409,88 @@ Underfull \hbox (badness 1112) in paragraph at lines 391--391
|
||||
the automorphism-free count
|
||||
[]
|
||||
|
||||
[6]
|
||||
[6] [7]
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 497.
|
||||
(hyperref) removing `math shift' on input line 536.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 497.
|
||||
(hyperref) removing `math shift' on input line 536.
|
||||
|
||||
[7]
|
||||
<figures/n21_duals.png, id=137, 1373.13pt x 867.24pt>
|
||||
[8]
|
||||
<figures/n21_duals.png, id=143, 1373.13pt x 867.24pt>
|
||||
File: figures/n21_duals.png Graphic file (type png)
|
||||
<use figures/n21_duals.png>
|
||||
Package pdftex.def Info: figures/n21_duals.png used on input line 557.
|
||||
Package pdftex.def Info: figures/n21_duals.png used on input line 596.
|
||||
(pdftex.def) Requested size: 360.0pt x 227.35617pt.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 570.
|
||||
(hyperref) removing `math shift' on input line 609.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 570.
|
||||
(hyperref) removing `math shift' on input line 609.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 570.
|
||||
(hyperref) removing `math shift' on input line 609.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 570.
|
||||
(hyperref) removing `math shift' on input line 609.
|
||||
|
||||
[8]
|
||||
<figures/fig210_dual.png, id=144, 542.025pt x 542.025pt>
|
||||
[9 <./figures/n21_duals.png>]
|
||||
<figures/fig210_dual.png, id=152, 542.025pt x 542.025pt>
|
||||
File: figures/fig210_dual.png Graphic file (type png)
|
||||
<use figures/fig210_dual.png>
|
||||
Package pdftex.def Info: figures/fig210_dual.png used on input line 619.
|
||||
Package pdftex.def Info: figures/fig210_dual.png used on input line 658.
|
||||
(pdftex.def) Requested size: 251.9989pt x 251.99767pt.
|
||||
[9 <./figures/n21_duals.png>]
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 635.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 635.
|
||||
(hyperref) removing `math shift' on input line 674.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 635.
|
||||
(hyperref) removing `math shift' on input line 674.
|
||||
|
||||
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 635.
|
||||
(hyperref) removing `math shift' on input line 674.
|
||||
|
||||
|
||||
Overfull \hbox (9.14177pt too wide) in paragraph at lines 648--656
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 674.
|
||||
|
||||
|
||||
Overfull \hbox (9.14177pt too wide) in paragraph at lines 687--695
|
||||
\OT1/cmr/m/n/10 The $\OML/cmm/m/it/10 n \OT1/cmr/m/n/10 = 23$ row re-com-putes
|
||||
Faulkner--Younger's min-i-mal-ity (no cycli-cally $5$-connected
|
||||
[]
|
||||
|
||||
[10 <./figures/fig210_dual.png>]
|
||||
<figures/core_n25_dual.png, id=160, 578.16pt x 578.16pt>
|
||||
[10]
|
||||
Underfull \vbox (badness 1831) has occurred while \output is active []
|
||||
|
||||
[11 <./figures/fig210_dual.png>]
|
||||
<figures/core_n25_dual.png, id=166, 578.16pt x 578.16pt>
|
||||
File: figures/core_n25_dual.png Graphic file (type png)
|
||||
<use figures/core_n25_dual.png>
|
||||
Package pdftex.def Info: figures/core_n25_dual.png used on input line 693.
|
||||
Package pdftex.def Info: figures/core_n25_dual.png used on input line 732.
|
||||
(pdftex.def) Requested size: 251.9989pt x 251.9916pt.
|
||||
[11] [12 <./figures/core_n25_dual.png>]
|
||||
[13] (./paper.aux)
|
||||
[12 <./figures/core_n25_dual.png>] [13] (./paper.aux)
|
||||
Package rerunfilecheck Info: File `paper.out' has not changed.
|
||||
(rerunfilecheck) Checksum: D310C1D6D9F73494FC676A3DD19E31E8;2030.
|
||||
)
|
||||
Here is how much of TeX's memory you used:
|
||||
9791 strings out of 478268
|
||||
151651 string characters out of 5846347
|
||||
455389 words of memory out of 5000000
|
||||
27676 multiletter control sequences out of 15000+600000
|
||||
9793 strings out of 478268
|
||||
151677 string characters out of 5846347
|
||||
455969 words of memory out of 5000000
|
||||
27677 multiletter control sequences out of 15000+600000
|
||||
475834 words of font info for 54 fonts, out of 8000000 for 9000
|
||||
1302 hyphenation exceptions out of 8191
|
||||
69i,9n,76p,822b,450s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
69i,9n,76p,822b,421s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb>
|
||||
@@ -504,10 +506,10 @@ ive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/local/texli
|
||||
ve/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/texlive
|
||||
/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/texlive/
|
||||
2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
|
||||
Output written on paper.pdf (13 pages, 1384388 bytes).
|
||||
Output written on paper.pdf (13 pages, 1386477 bytes).
|
||||
PDF statistics:
|
||||
253 PDF objects out of 1000 (max. 8388607)
|
||||
192 compressed objects within 2 object streams
|
||||
48 named destinations out of 1000 (max. 500000)
|
||||
256 PDF objects out of 1000 (max. 8388607)
|
||||
195 compressed objects within 2 object streams
|
||||
49 named destinations out of 1000 (max. 500000)
|
||||
116 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
|
||||
@@ -492,6 +492,45 @@ $n = 21$ and there are exactly $6$ of them. Below $n = 21$ every
|
||||
maximal planar graph is an intertwining tree, which is why the
|
||||
disjunction holds trivially in that range.
|
||||
|
||||
The intertwining-tree disjunct therefore carries the conjecture by itself
|
||||
for all small $n$, but this leaves open how much of the load the
|
||||
bridge-derived disjunct is independently able to bear. To measure that we
|
||||
classified every triangulation at $6 \leq n \leq 10$ as bridge-derived or
|
||||
not, deciding bridge-derivedness exhaustively: a triangulation is
|
||||
bridge-derived iff some valid parity partition admits an Even Level Graph
|
||||
in its backward bridge-switch orbit (a search feasible only at these
|
||||
sizes). Table~\ref{tab:bridge-census} records the result. Three features
|
||||
stand out. First, the bridge-derived disjunct is substantive but far from
|
||||
universal on its own: its share of all triangulations falls steadily, from
|
||||
all of them at $n = 6$ to under two-thirds by $n = 10$. Second, the
|
||||
disjunction never relies on it in this range -- the \emph{intertwining
|
||||
only} column counts triangulations covered by the tree disjunct alone, and
|
||||
it grows, while no triangulation here is bridge-derived without also being
|
||||
an intertwining tree. Third, and consistent with the conjecture, the
|
||||
\emph{neither} column is identically zero throughout.
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{cccccc}
|
||||
$n$ & triangulations & bridge-derived & \% & intertwining only & neither \\\hline
|
||||
$6$ & $2$ & $2$ & $100.0$ & $0$ & $0$ \\
|
||||
$7$ & $5$ & $4$ & $80.0$ & $1$ & $0$ \\
|
||||
$8$ & $14$ & $12$ & $85.7$ & $2$ & $0$ \\
|
||||
$9$ & $50$ & $36$ & $72.0$ & $14$ & $0$ \\
|
||||
$10$ & $233$ & $146$ & $62.7$ & $87$ & $0$ \\
|
||||
\end{tabular}
|
||||
\caption{Bridge-derived census for $6 \leq n \leq 10$. \emph{bridge-derived}
|
||||
counts plane-triangulation iso classes that are bridge-derived level graphs
|
||||
of some Even Level Graph, decided by exhaustive backward bridge-switch
|
||||
search over all valid parity partitions; \% is its fraction of all
|
||||
triangulations. \emph{intertwining only} counts those that are intertwining
|
||||
trees but not bridge-derived; \emph{neither} counts those covered by no
|
||||
disjunct. Every triangulation in this range is an intertwining tree, and
|
||||
every bridge-derived one is too, so bridge-derived $\subseteq$ intertwining
|
||||
tree here.}
|
||||
\label{tab:bridge-census}
|
||||
\end{table}
|
||||
|
||||
\subsection*{The boundary case $n = 21$}
|
||||
|
||||
The first triangulations that are \emph{not} intertwining trees are the
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
Does the richness invariant survive BRANCHING?
|
||||
|
||||
For a separating cycle C bounding a disk G_C (away from the source), the achievable
|
||||
outer-interface set is
|
||||
|
||||
Phi(C) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{F(G_C)},
|
||||
sum_{f ∋ w} lambda(f) ≡ 0 for every
|
||||
truly-interior vertex w of G_C }.
|
||||
|
||||
This is the exact value the recursive transfer operator produces at C (interior
|
||||
consistency = all the descendant gluings already performed; seam/boundary vertices
|
||||
are deferred, exactly as in the recursion). We compute Phi(C) by constrained
|
||||
enumeration over real triangulations and test the candidate self-similar invariant
|
||||
|
||||
non-empty & closed under sign flip & full single-position marginals
|
||||
|
||||
separately at BRANCH nodes (region encloses >=2 disjoint deeper sub-tires) and at
|
||||
LINEAR nodes (one child), to see whether branching breaks it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import defaultdict, deque
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
|
||||
def delaunay(n, rng):
|
||||
pts = rng.random((n, 2))
|
||||
tri = Delaunay(pts)
|
||||
faces = [tuple(int(x) for x in s) for s in tri.simplices]
|
||||
hull = set(int(v) for e in tri.convex_hull for v in e)
|
||||
return faces, hull
|
||||
|
||||
|
||||
def build(faces):
|
||||
adj = defaultdict(set)
|
||||
efaces = defaultdict(list)
|
||||
vfaces = defaultdict(list)
|
||||
for fi, (a, b, c) in enumerate(faces):
|
||||
adj[a] |= {b, c}; adj[b] |= {a, c}; adj[c] |= {a, b}
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
efaces[frozenset(e)].append(fi)
|
||||
for v in (a, b, c):
|
||||
vfaces[v].append(fi)
|
||||
fadj = [set() for _ in faces]
|
||||
for fl in efaces.values():
|
||||
for i in fl:
|
||||
for j in fl:
|
||||
if i != j:
|
||||
fadj[i].add(j)
|
||||
return adj, fadj, vfaces
|
||||
|
||||
|
||||
def bfs(adj, src):
|
||||
lev = {src: 0}; q = deque([src])
|
||||
while q:
|
||||
u = q.popleft()
|
||||
for w in adj[u]:
|
||||
if w not in lev:
|
||||
lev[w] = lev[u] + 1; q.append(w)
|
||||
return lev
|
||||
|
||||
|
||||
def components(face_ids, fadj):
|
||||
idset = set(face_ids)
|
||||
seen = set(); comps = []
|
||||
for s in face_ids:
|
||||
if s in seen:
|
||||
continue
|
||||
comp = []; stack = [s]; seen.add(s)
|
||||
while stack:
|
||||
u = stack.pop(); comp.append(u)
|
||||
for w in fadj[u]:
|
||||
if w in idset and w not in seen:
|
||||
seen.add(w); stack.append(w)
|
||||
comps.append(comp)
|
||||
return comps
|
||||
|
||||
|
||||
def sign_closed(S):
|
||||
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
|
||||
|
||||
|
||||
def marginals_full(S, k):
|
||||
return all({s[i] for s in S} == {0, 1, 2} for i in range(k))
|
||||
|
||||
|
||||
def phi_of_region(comp_faces, faces, vfaces, lev, d, cap):
|
||||
"""Constrained-enumeration Phi on the outer (level-d) cycle of a region."""
|
||||
Gc = comp_faces
|
||||
if not (1 <= len(Gc) <= cap):
|
||||
return None
|
||||
Gcset = set(Gc)
|
||||
verts = sorted(set(v for fi in Gc for v in faces[fi]))
|
||||
# truly-interior: every global incident face is inside G_C (=> level > d)
|
||||
interior = [v for v in verts if all(f in Gcset for f in vfaces[v])]
|
||||
boundary_C = [v for v in verts if lev[v] == d and v not in interior]
|
||||
if not boundary_C:
|
||||
return None
|
||||
F = len(Gc)
|
||||
fidx = {fi: j for j, fi in enumerate(Gc)}
|
||||
# incidence rows
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
for r, w in enumerate(interior):
|
||||
for fi in vfaces[w]:
|
||||
if fi in Gcset:
|
||||
Bint[r, fidx[fi]] = 1
|
||||
Cinc = np.zeros((len(boundary_C), F), dtype=np.int64)
|
||||
for r, v in enumerate(boundary_C):
|
||||
for fi in vfaces[v]:
|
||||
if fi in Gcset:
|
||||
Cinc[r, fidx[fi]] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if len(interior):
|
||||
ok = np.all((labs @ Bint.T) % 3 == 0, axis=1)
|
||||
labs = labs[ok]
|
||||
if labs.shape[0] == 0:
|
||||
return set(), len(boundary_C)
|
||||
outer = (labs @ Cinc.T) % 3
|
||||
return set(map(tuple, np.unique(outer, axis=0))), len(boundary_C)
|
||||
|
||||
|
||||
def main():
|
||||
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
nprng = np.random.default_rng(seed)
|
||||
CAP = 18
|
||||
|
||||
stats = {True: [0, 0, 0, 0], False: [0, 0, 0, 0]} # branch: [n, nonempty, sign, marg]
|
||||
examples_fail = []
|
||||
|
||||
for _ in range(300):
|
||||
faces, hull = delaunay(int(nprng.integers(14, 34)), nprng)
|
||||
adj, fadj, vfaces = build(faces)
|
||||
lev = bfs(adj, min(hull))
|
||||
if len(lev) != len(adj):
|
||||
continue
|
||||
depth = [min(lev[v] for v in faces[fi]) for fi in range(len(faces))]
|
||||
maxd = max(depth)
|
||||
for d in range(1, maxd + 1):
|
||||
fge = [fi for fi in range(len(faces)) if depth[fi] >= d]
|
||||
for comp in components(fge, fadj):
|
||||
if not (1 <= len(comp) <= CAP):
|
||||
continue
|
||||
deeper = [fi for fi in comp if depth[fi] >= d + 1]
|
||||
n_children = len(components(deeper, fadj))
|
||||
is_branch = n_children >= 2
|
||||
res = phi_of_region(comp, faces, vfaces, lev, d, CAP)
|
||||
if res is None:
|
||||
continue
|
||||
S, k = res
|
||||
rec = stats[is_branch]
|
||||
rec[0] += 1
|
||||
rec[1] += bool(S)
|
||||
rec[2] += (bool(S) and sign_closed(S))
|
||||
rec[3] += (bool(S) and marginals_full(S, k))
|
||||
if S and not marginals_full(S, k) and len(examples_fail) < 6:
|
||||
examples_fail.append((is_branch, n_children, len(comp), k,
|
||||
len(S)))
|
||||
|
||||
for branch in (False, True):
|
||||
n, ne, sg, mg = stats[branch]
|
||||
tag = "BRANCH (>=2 children)" if branch else "LINEAR (1 child)"
|
||||
if n:
|
||||
print(f"{tag}: {n} regions")
|
||||
print(f" non-empty : {ne}/{n} ({100*ne/n:.1f}%)")
|
||||
print(f" sign-closed : {sg}/{n} ({100*sg/n:.1f}%)")
|
||||
print(f" marginals-full : {mg}/{n} ({100*mg/n:.1f}%)")
|
||||
else:
|
||||
print(f"{tag}: 0 regions")
|
||||
if examples_fail:
|
||||
print("\n marginals-NOT-full examples (branch?,n_children,|G_C|,|C|,|Phi|):")
|
||||
for e in examples_fail:
|
||||
print(f" {e}")
|
||||
else:
|
||||
print("\n richness (incl. full marginals) held on every region tested.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Robustness check for the 2^(n-2) constraint floor over DIVERSE (not just stacked)
|
||||
triangulated disks.
|
||||
|
||||
maximally_constrain.py searches Apollonian-stacked disks only, which miss
|
||||
non-stacked triangulations (e.g. a wheel with a high-degree center). Here we
|
||||
generate disks from random interior points + Delaunay, with boundary points in
|
||||
convex but NON-cocircular position (cocircular boundary points are a Delaunay
|
||||
degeneracy that yields INVALID disks -- missing a boundary edge -- and spuriously
|
||||
report sub-floor |Phi|). Every disk is validated (2k+n-2 faces, all n boundary
|
||||
edges present) before Phi is computed.
|
||||
|
||||
Finding: min |Phi| over validated diverse disks is exactly 2^(n-2), attained by
|
||||
the interior-free triangulation; deeper structure never goes below it (and the
|
||||
central-apex wheel actually ENLARGES Phi: 5 vs the fan's 4 on the 4-cycle).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
def disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.15 * rng.random(n) # convex but not cocircular
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
if k:
|
||||
r = 0.75 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
ipts = np.c_[r * np.cos(t), r * np.sin(t)]
|
||||
pts = np.vstack([bpts, ipts])
|
||||
else:
|
||||
pts = bpts
|
||||
tri = Delaunay(pts)
|
||||
return [tuple(int(x) for x in s) for s in tri.simplices]
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def phi(faces, n, k):
|
||||
F = len(faces)
|
||||
interior = list(range(n, n + k))
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[interior.index(v), j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if interior:
|
||||
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
|
||||
if labs.shape[0] == 0:
|
||||
return set()
|
||||
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
|
||||
|
||||
|
||||
def main():
|
||||
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6]
|
||||
rng = np.random.default_rng(1)
|
||||
print("Min |Phi| over validated diverse (Delaunay) disks\n")
|
||||
for n in ns:
|
||||
best = 10 ** 9; bk = None; nval = 0; max_seen = 0
|
||||
for k in range(0, 7):
|
||||
for _ in range(250):
|
||||
faces = disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 20:
|
||||
continue
|
||||
nval += 1
|
||||
P = phi(faces, n, k)
|
||||
if P:
|
||||
max_seen = max(max_seen, len(P))
|
||||
if len(P) < best:
|
||||
best = len(P); bk = k
|
||||
print(f" n={n}: {nval} valid disks min|Phi|={best} (k={bk}) "
|
||||
f"max|Phi|={max_seen} 2^(n-2)={2**(n-2)} "
|
||||
f"below-floor={best < 2**(n-2)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
The open case for the 2^(n-2) lower bound is the IRREDUCIBLE disk: k>=1 interior
|
||||
vertices, all of degree >=4 (un-stacking already settles everything reducible to
|
||||
a degree-3 vertex). This probe isolates irreducible disks and asks:
|
||||
|
||||
(a) does the floor |Phi| >= 2^(n-2) hold there (it must, but it's the open case);
|
||||
(b) is it STRICT (|Phi| > 2^(n-2)) -- if irreducible disks never sit ON the floor,
|
||||
the proof only needs ">= floor" via "any nontrivial slack";
|
||||
(c) how many UNIVERSAL toggles each has -- a "boundary-only" face (all 3 vertices
|
||||
on C) can be flipped feasibly regardless of the labelling, giving a guaranteed
|
||||
doubling. n-2 independent ones would prove the bound outright. We count them
|
||||
to see whether universal toggles alone can carry the irreducible case (a wheel
|
||||
has zero, so we expect NOT).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
def disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.18 * rng.random(n)
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
r = 0.78 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
|
||||
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def interior_degrees(faces, n):
|
||||
deg = Counter()
|
||||
for f in faces:
|
||||
for v in f:
|
||||
if v >= n:
|
||||
deg[v] += 1
|
||||
return deg
|
||||
|
||||
|
||||
def phi(faces, n):
|
||||
interior = sorted(v for f in faces for v in f if v >= n)
|
||||
interior = sorted(set(interior))
|
||||
F = len(faces)
|
||||
if F > 18:
|
||||
return None
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
iidx = {w: r for r, w in enumerate(interior)}
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iidx[v], j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if interior:
|
||||
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
|
||||
if labs.shape[0] == 0:
|
||||
return set()
|
||||
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
|
||||
|
||||
|
||||
def boundary_only_faces(faces, n):
|
||||
return sum(1 for (a, b, c) in faces if a < n and b < n and c < n)
|
||||
|
||||
|
||||
def main():
|
||||
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
rng = np.random.default_rng(seed)
|
||||
by_n = {}
|
||||
n_irred = 0
|
||||
floor_fail = 0
|
||||
on_floor = 0 # irreducible AND exactly at 2^(n-2)
|
||||
bof_ge = 0 # irreducible with >= n-2 boundary-only faces
|
||||
bof_zero = 0
|
||||
min_bof = 99
|
||||
|
||||
for _ in range(6000):
|
||||
n = int(rng.integers(4, 7)); k = int(rng.integers(1, 4))
|
||||
faces = disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 16:
|
||||
continue
|
||||
deg = interior_degrees(faces, n)
|
||||
if any(deg[v] < 4 for v in deg) or len(deg) < k:
|
||||
continue # reducible (has a degree-3 vertex)
|
||||
P = phi(faces, n)
|
||||
if P is None or not P:
|
||||
continue
|
||||
n_irred += 1
|
||||
floor = 2 ** (n - 2)
|
||||
if len(P) < floor:
|
||||
floor_fail += 1
|
||||
if len(P) == floor:
|
||||
on_floor += 1
|
||||
bof = boundary_only_faces(faces, n)
|
||||
min_bof = min(min_bof, bof)
|
||||
bof_ge += (bof >= n - 2)
|
||||
bof_zero += (bof == 0)
|
||||
d = by_n.setdefault(n, {"cnt": 0, "min": 10**9, "floor": floor})
|
||||
d["cnt"] += 1
|
||||
d["min"] = min(d["min"], len(P))
|
||||
|
||||
print(f"irreducible disks (k>=1, all interior degree >=4): {n_irred}\n")
|
||||
for n in sorted(by_n):
|
||||
d = by_n[n]
|
||||
print(f" n={n}: {d['cnt']:5d} disks min|Phi|={d['min']} "
|
||||
f"2^(n-2)={d['floor']} floor-held={d['min']>=d['floor']}")
|
||||
print(f"\n floor violations (|Phi| < 2^(n-2)) : {floor_fail}")
|
||||
print(f" irreducible disks sitting ON the floor: {on_floor} "
|
||||
f"(if 0, irreducible => strictly above floor)")
|
||||
print(f" universal toggles (boundary-only faces):")
|
||||
print(f" >= n-2 such faces : {bof_ge}/{n_irred}")
|
||||
print(f" exactly 0 : {bof_zero}/{n_irred} (min over all = {min_bof})")
|
||||
print(" => universal toggles alone "
|
||||
+ ("CAN" if bof_zero == 0 else "CANNOT")
|
||||
+ " carry the irreducible case.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Local heart of the size-reduction lemma.
|
||||
|
||||
Removing a degree-d interior vertex v swaps its star (d faces, contribution
|
||||
mu_{j-1}+mu_j at link vertex u_j, plus the v-constraint sum mu ≡ 0) for a fan
|
||||
(d-2 faces). Outside is shared, entering only as prescribed residues t_j at the
|
||||
INTERIOR link vertices. The boundary-link contribution sets are
|
||||
|
||||
Star(t) = {(mu_{j-1}+mu_j)_{u_j boundary} : mu in {+-1}^d, sum mu ≡ 0,
|
||||
mu_{j-1}+mu_j ≡ t_j for interior u_j}
|
||||
Fan_r(t)= {(fan_j)_{u_j boundary} : nu in {+-1}^{d-2},
|
||||
fan_j ≡ t_j for interior u_j}
|
||||
(fan rooted at link vertex r; fan_j = contribution of the fan to u_j)
|
||||
|
||||
The reduction may CHOOSE the fan root, so it wants min_r |Fan_r(t)|. For the
|
||||
induction |Phi(D-v)| <= |Phi(D)| to be locally supported we need, for every
|
||||
interior-mask and every t:
|
||||
|
||||
|Star(t)| >= min_r |Fan_r(t)| (and Star(t) nonempty whenever some Fan is)
|
||||
|
||||
We check this exhaustively for small d. (+-1 encoded as 1,2 mod 3.)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from itertools import product
|
||||
|
||||
|
||||
def star_contrib(d, interior, t):
|
||||
"""interior: set of link indices that are interior; t: dict j->residue for
|
||||
interior j. Returns set of boundary contribution tuples."""
|
||||
bnd = [j for j in range(d) if j not in interior]
|
||||
out = set()
|
||||
for mu in product((1, 2), repeat=d): # +-1 as 1,2
|
||||
if sum(mu) % 3 != 0: # v-constraint
|
||||
continue
|
||||
contrib = [(mu[(j - 1) % d] + mu[j]) % 3 for j in range(d)]
|
||||
if any(contrib[j] != t[j] for j in interior):
|
||||
continue
|
||||
out.add(tuple(contrib[j] for j in bnd))
|
||||
return out
|
||||
|
||||
|
||||
def fan_contrib(d, root, interior, t):
|
||||
"""Fan of the link d-gon rooted at vertex `root`. Reindex so root=0."""
|
||||
bnd = [j for j in range(d) if j not in interior]
|
||||
# contribution of a fan (root r) to vertex u_j, for nu over the d-2 fan faces.
|
||||
# In root-0 coordinates faces are (0,j,j+1) for j=1..d-2 with labels nu[1..d-2].
|
||||
# contribution: u0 -> sum(nu); u1 -> nu1; u_{d-1} -> nu_{d-2};
|
||||
# u_j (1<j<d-1) -> nu_{j-1}+nu_j.
|
||||
out = set()
|
||||
for nu in product((1, 2), repeat=d - 2): # nu indexed 1..d-2 -> 0..d-3
|
||||
nuf = {j: nu[j - 1] for j in range(1, d - 1)}
|
||||
contrib = [0] * d
|
||||
# in root coordinates u'_i = u_{(root+i) mod d}
|
||||
c = [0] * d
|
||||
c[0] = sum(nu) % 3
|
||||
c[1] = nuf[1] % 3
|
||||
c[d - 1] = nuf[d - 2] % 3
|
||||
for j in range(2, d - 1):
|
||||
c[j] = (nuf[j - 1] + nuf[j]) % 3
|
||||
# map back: actual vertex = (root+i) mod d gets c[i]
|
||||
for i in range(d):
|
||||
contrib[(root + i) % d] = c[i]
|
||||
if any(contrib[j] != t[j] for j in interior):
|
||||
continue
|
||||
out.add(tuple(contrib[j] for j in bnd))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
print("Local claim: |Star(t)| >= min_root |Fan_root(t)| for all interior-masks, t\n")
|
||||
fails = 0
|
||||
checked = 0
|
||||
worst = None
|
||||
for d in range(3, 8):
|
||||
for im in range(0, 1 << d):
|
||||
interior = {j for j in range(d) if im >> j & 1}
|
||||
if len(interior) == d:
|
||||
continue # need a boundary vertex to see
|
||||
int_list = sorted(interior)
|
||||
for tvals in product((0, 1, 2), repeat=len(interior)):
|
||||
t = dict(zip(int_list, tvals))
|
||||
S = star_contrib(d, interior, t)
|
||||
fans = [fan_contrib(d, r, interior, t) for r in range(d)]
|
||||
# the reduction needs Star nonempty whenever it removes v; and
|
||||
# it picks the fan, so compare to the SMALLEST fan that is itself
|
||||
# achievable (nonempty) -- an empty fan means that root is invalid.
|
||||
nonempty_fans = [len(f) for f in fans if f]
|
||||
if not S:
|
||||
# star infeasible: then v cannot carry this context. Only a
|
||||
# problem if some fan IS feasible (then D-v has a seq D lacks).
|
||||
if nonempty_fans:
|
||||
fails += 1
|
||||
continue
|
||||
checked += 1
|
||||
if nonempty_fans:
|
||||
mn = min(nonempty_fans)
|
||||
if len(S) < mn:
|
||||
fails += 1
|
||||
if worst is None or len(S) - mn < worst[0]:
|
||||
worst = (len(S) - mn, d, sorted(interior), t,
|
||||
len(S), mn)
|
||||
print(f" configurations checked: {checked}")
|
||||
print(f" violations of |Star| >= min nonempty |Fan|: {fails}")
|
||||
if worst:
|
||||
print(f" worst gap (|Star|-minFan, d, interior, t, |Star|, minFan): {worst}")
|
||||
if fails == 0:
|
||||
print(" => LOCAL CLAIM HOLDS: the star always dominates the best fan.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Construct the triangulated disk (= nested tire substructure) that MAXIMALLY
|
||||
constrains its outer cycle.
|
||||
|
||||
For a triangulated disk D with boundary cycle C = (0..n-1), the achievable outer
|
||||
Heawood set is
|
||||
|
||||
Phi(D) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{faces},
|
||||
sum_{f ∋ w} lambda(f) ≡ 0 for every interior vertex w } .
|
||||
|
||||
Phi depends only on the disk triangulation (no BFS/tree needed). We want the disk
|
||||
minimising |Phi| -- the worst case for the pigeonhole. Note Phi is always
|
||||
sign-closed and non-empty, so |Phi| >= 1, and |Phi| = 1 forces Phi = { all-zeros }.
|
||||
|
||||
Key local fact: a degree-3 interior vertex (one Apollonian stack) has incident
|
||||
faces f1,f2,f3 with lambda(f1)+lambda(f2)+lambda(f3) ≡ 0 mod 3 over +/-1 values,
|
||||
which forces f1=f2=f3. So stacking chains equalities and collapses Phi.
|
||||
|
||||
We (a) randomly search disks built by Apollonian stacking, and (b) try a
|
||||
deterministic deep-stack construction, reporting the smallest Phi found.
|
||||
"""
|
||||
|
||||
import random
|
||||
import sys
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def fan_triangulation(n):
|
||||
"""n-gon (0..n-1) triangulated as a fan from vertex 0. No interior vertex."""
|
||||
return [(0, i, i + 1) for i in range(1, n - 1)]
|
||||
|
||||
|
||||
def stack(faces, idx, v):
|
||||
a, b, c = faces[idx]
|
||||
faces[idx] = (a, b, v)
|
||||
faces.append((b, c, v))
|
||||
faces.append((a, c, v))
|
||||
|
||||
|
||||
def phi(faces, n, cap):
|
||||
"""Phi on boundary 0..n-1; interior = vertices >= n."""
|
||||
verts = set(v for f in faces for v in f)
|
||||
interior = sorted(v for v in verts if v >= n)
|
||||
F = len(faces)
|
||||
if F > cap:
|
||||
return None
|
||||
# incidence
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
iindex = {w: r for r, w in enumerate(interior)}
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iindex[v], j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if len(interior):
|
||||
keep = np.all((labs @ Bint.T) % 3 == 0, axis=1)
|
||||
labs = labs[keep]
|
||||
if labs.shape[0] == 0:
|
||||
return set()
|
||||
outer = (labs @ Cinc.T) % 3
|
||||
return set(map(tuple, np.unique(outer, axis=0)))
|
||||
|
||||
|
||||
def disp(s):
|
||||
return tuple(-1 if int(x) == 2 else int(x) for x in s)
|
||||
|
||||
|
||||
def gf3_rank(rows):
|
||||
M = [[int(x) % 3 for x in r] for r in rows]
|
||||
if not M:
|
||||
return 0
|
||||
nc = len(M[0]); r = 0
|
||||
for c in range(nc):
|
||||
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
|
||||
if piv is None:
|
||||
continue
|
||||
M[r], M[piv] = M[piv], M[r]
|
||||
inv = M[r][c] % 3
|
||||
M[r] = [(x * inv) % 3 for x in M[r]]
|
||||
for i in range(len(M)):
|
||||
if i != r and M[i][c] % 3:
|
||||
fct = M[i][c] % 3
|
||||
M[i] = [(M[i][k] - fct * M[r][k]) % 3 for k in range(nc)]
|
||||
r += 1
|
||||
if r == len(M):
|
||||
break
|
||||
return r
|
||||
|
||||
|
||||
def describe(P):
|
||||
P = list(P)
|
||||
sign_closed = all(tuple((3 - x) % 3 for x in s) in set(P) for s in P)
|
||||
s0 = P[0]
|
||||
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in P]
|
||||
rank = gf3_rank(D)
|
||||
affine = (len(P) == 3 ** rank)
|
||||
pow2 = (len(P) & (len(P) - 1)) == 0
|
||||
return (f"sign-closed={sign_closed} affine-GF3={affine} "
|
||||
f"|Phi|={len(P)} (power-of-2={pow2}) hull-dim={rank}")
|
||||
|
||||
|
||||
def random_disk(n, n_stacks, rng):
|
||||
faces = fan_triangulation(n)
|
||||
nxt = n
|
||||
for _ in range(n_stacks):
|
||||
stack(faces, rng.randrange(len(faces)), nxt)
|
||||
nxt += 1
|
||||
return faces
|
||||
|
||||
|
||||
def deep_stack_disk(n, n_stacks):
|
||||
"""Always stack into the most-recently created face -> deep equality chain."""
|
||||
faces = fan_triangulation(n)
|
||||
nxt = n
|
||||
for _ in range(n_stacks):
|
||||
stack(faces, len(faces) - 1, nxt)
|
||||
nxt += 1
|
||||
return faces
|
||||
|
||||
|
||||
def search(n, cap=18, trials=400, seed=0):
|
||||
rng = random.Random(seed)
|
||||
best = (10 ** 9, None, None)
|
||||
max_stacks = (cap - (n - 2)) // 2
|
||||
# random search
|
||||
for _ in range(trials):
|
||||
k = rng.randint(0, max_stacks)
|
||||
faces = random_disk(n, k, rng)
|
||||
P = phi(faces, n, cap)
|
||||
if P is None:
|
||||
continue
|
||||
if len(P) < best[0]:
|
||||
best = (len(P), k, P)
|
||||
# deterministic deep stack at max depth
|
||||
for k in range(max_stacks + 1):
|
||||
faces = deep_stack_disk(n, k)
|
||||
P = phi(faces, n, cap)
|
||||
if P is not None and len(P) < best[0]:
|
||||
best = (len(P), k, P)
|
||||
size, k, P = best
|
||||
print(f"n={n}: min |Phi| = {size} (= 2^(n-2) = {2**(n-2)}?) "
|
||||
f"interior vertices = {k}, max stacks at cap {cap} = {max_stacks}")
|
||||
print(f" {describe(P)}")
|
||||
for s in sorted(P)[:6]:
|
||||
print(f" {disp(s)}")
|
||||
if len(P) > 6:
|
||||
print(f" ... (+{len(P)-6} more)")
|
||||
return size
|
||||
|
||||
|
||||
def main():
|
||||
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6, 7]
|
||||
print("Searching for maximally-constraining disks (min |Phi|)\n")
|
||||
for n in ns:
|
||||
# bigger cap for small n
|
||||
cap = 18 if n <= 6 else 16
|
||||
search(n, cap=cap)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Test the MONOTONICITY LEMMA behind the 2^(n-2) lower bound:
|
||||
|
||||
adding an interior vertex never DECREASES |Phi|
|
||||
(equivalently Phi(D') subset Phi(D) when D = D' + one interior vertex).
|
||||
|
||||
If true, every disk reduces to the k=0 base case without increasing Phi, so
|
||||
|Phi(D)| >= |Phi(D_0)| = 2^(n-2). A single insertion that shrinks Phi (or breaks
|
||||
the inclusion) would refute the proof strategy.
|
||||
|
||||
We build a validated base disk D' (Delaunay, convex non-cocircular boundary), then
|
||||
insert a vertex two ways:
|
||||
* degree-3 stack into a face (proved: should give equality)
|
||||
* degree-4 open of an internal edge (a,b)|(c,d) (the first genuinely open case)
|
||||
and compare Phi(D') to Phi(D).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
def base_disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.15 * rng.random(n)
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
if k:
|
||||
r = 0.7 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
|
||||
else:
|
||||
pts = bpts
|
||||
faces = [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
|
||||
return faces
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def phi(faces, n):
|
||||
verts = set(v for f in faces for v in f)
|
||||
interior = sorted(v for v in verts if v >= n)
|
||||
F = len(faces)
|
||||
if F > 18:
|
||||
return None
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
iidx = {w: r for r, w in enumerate(interior)}
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iidx[v], j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if interior:
|
||||
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
|
||||
if labs.shape[0] == 0:
|
||||
return set()
|
||||
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
|
||||
|
||||
|
||||
def insert_deg3(faces, fi, v):
|
||||
a, b, c = faces[fi]
|
||||
out = [f for i, f in enumerate(faces) if i != fi]
|
||||
out += [(a, b, v), (b, c, v), (a, c, v)]
|
||||
return out
|
||||
|
||||
|
||||
def insert_deg4(faces, n):
|
||||
"""Open an internal edge (a,b) shared by faces (a,b,c),(a,b,d): replace those
|
||||
two faces with the 4-star of a new center over the quad a-c-b-d."""
|
||||
ef = defaultdict(list)
|
||||
for i, (a, b, c) in enumerate(faces):
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ef[frozenset(e)].append(i)
|
||||
for e, fl in ef.items():
|
||||
if len(fl) != 2:
|
||||
continue
|
||||
a, b = tuple(e)
|
||||
if (a < n and b < n and (b - a) % n in (1, n - 1)):
|
||||
continue # boundary edge
|
||||
f1, f2 = faces[fl[0]], faces[fl[1]]
|
||||
c = next(x for x in f1 if x not in (a, b))
|
||||
d = next(x for x in f2 if x not in (a, b))
|
||||
if c == d:
|
||||
continue
|
||||
v = max(max(f) for f in faces) + 1
|
||||
out = [f for i, f in enumerate(faces) if i not in fl]
|
||||
out += [(a, c, v), (c, b, v), (b, d, v), (d, a, v)]
|
||||
return out, v
|
||||
return None, None
|
||||
|
||||
|
||||
def main():
|
||||
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
rng = np.random.default_rng(seed)
|
||||
viol_size = 0 # |Phi(D)| < |Phi(D')| (monotonicity broken)
|
||||
viol_incl = 0 # Phi(D') not subset Phi(D)
|
||||
tested = 0
|
||||
deg3_equal = 0; deg3_tot = 0
|
||||
deg4_strict = 0; deg4_tot = 0
|
||||
examples = []
|
||||
|
||||
for _ in range(500):
|
||||
n = int(rng.integers(4, 7)); k = int(rng.integers(0, 4))
|
||||
faces = base_disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 14:
|
||||
continue
|
||||
Pp = phi(faces, n)
|
||||
if Pp is None:
|
||||
continue
|
||||
# degree-3 insertions: every face
|
||||
for fi in range(len(faces)):
|
||||
D = insert_deg3(faces, fi, max(max(f) for f in faces) + 1)
|
||||
P = phi(D, n)
|
||||
if P is None:
|
||||
continue
|
||||
tested += 1; deg3_tot += 1
|
||||
if not Pp <= P:
|
||||
viol_incl += 1
|
||||
if len(P) < len(Pp):
|
||||
viol_size += 1
|
||||
if len(examples) < 5:
|
||||
examples.append(("deg3", n, len(Pp), len(P)))
|
||||
if len(P) == len(Pp):
|
||||
deg3_equal += 1
|
||||
# one degree-4 insertion
|
||||
D4, v = insert_deg4(faces, n)
|
||||
if D4 is not None:
|
||||
P = phi(D4, n)
|
||||
if P is not None:
|
||||
tested += 1; deg4_tot += 1
|
||||
if not Pp <= P:
|
||||
viol_incl += 1
|
||||
if len(P) < len(Pp):
|
||||
viol_size += 1
|
||||
if len(examples) < 5:
|
||||
examples.append(("deg4", n, len(Pp), len(P)))
|
||||
if len(P) > len(Pp):
|
||||
deg4_strict += 1
|
||||
|
||||
print(f"insertions tested: {tested}")
|
||||
print(f" monotonicity violations (|Phi(D)| < |Phi(D')|): {viol_size}")
|
||||
print(f" inclusion violations (Phi(D') not subset Phi(D)): {viol_incl}")
|
||||
print(f" degree-3: {deg3_equal}/{deg3_tot} gave EXACT equality "
|
||||
f"(proved un-stacking => should be all)")
|
||||
print(f" degree-4: {deg4_strict}/{deg4_tot} strictly ENLARGED Phi")
|
||||
if examples:
|
||||
print(" VIOLATION examples (type,n,|Phi(D')|,|Phi(D)|):")
|
||||
for e in examples:
|
||||
print(f" {e}")
|
||||
else:
|
||||
print(" no violation: every insertion preserved or enlarged Phi, "
|
||||
"and Phi(D') subset Phi(D) throughout.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Proof strategy A for the irreducible lemma (|Phi| >= 2^(n-2)):
|
||||
|
||||
induction on k via a Phi-NON-INCREASING vertex removal. Monotonicity is false
|
||||
(some removals raise Phi), but we only need ONE good removal per disk: if every
|
||||
disk with k>=1 has an interior vertex v and a link-retriangulation with
|
||||
|Phi(D - v)| <= |Phi(D)|, then chaining down to k=0 gives |Phi(D)| >= 2^(n-2).
|
||||
|
||||
This probe: for each disk, try removing each interior vertex (retriangulating its
|
||||
link by a fan from every link vertex, keeping only valid retriangulations), and
|
||||
record whether the BEST removal satisfies |Phi(D-v)| <= |Phi(D)|. Reports the
|
||||
fraction of disks where such a removal EXISTS (strategy viable) vs disks where
|
||||
EVERY removal strictly raises Phi (strategy fails) -- and dumps the failures.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
def disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.18 * rng.random(n)
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
|
||||
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def phi_size(faces, n):
|
||||
interior = sorted(set(v for f in faces for v in f if v >= n))
|
||||
F = len(faces)
|
||||
if F > 16:
|
||||
return None
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
iidx = {w: r for r, w in enumerate(interior)}
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iidx[v], j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if interior:
|
||||
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
|
||||
if labs.shape[0] == 0:
|
||||
return 0
|
||||
return len(set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0))))
|
||||
|
||||
|
||||
def edges_of(faces):
|
||||
E = set()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
E.add(frozenset(e))
|
||||
return E
|
||||
|
||||
|
||||
def link_cycle(faces, v):
|
||||
opp = [tuple(x for x in f if x != v) for f in faces if v in f]
|
||||
adj = defaultdict(list)
|
||||
for a, b in opp:
|
||||
adj[a].append(b); adj[b].append(a)
|
||||
if any(len(adj[u]) != 2 for u in adj):
|
||||
return None # link not a simple cycle
|
||||
start = opp[0][0]; cyc = [start]; prev = None; cur = start
|
||||
while True:
|
||||
nxt = [x for x in adj[cur] if x != prev]
|
||||
if not nxt:
|
||||
return None
|
||||
nxt = nxt[0]
|
||||
if nxt == start:
|
||||
break
|
||||
cyc.append(nxt); prev, cur = cur, nxt
|
||||
if len(cyc) > len(adj):
|
||||
return None
|
||||
return cyc if len(cyc) == len(adj) else None
|
||||
|
||||
|
||||
def removals(faces, v, n):
|
||||
"""Yield valid (faces of D - v) over fan retriangulations of v's link."""
|
||||
cyc = link_cycle(faces, v)
|
||||
if cyc is None:
|
||||
return
|
||||
d = len(cyc)
|
||||
rest = [f for f in faces if v not in f]
|
||||
Erest = edges_of(rest)
|
||||
for s in range(d):
|
||||
order = cyc[s:] + cyc[:s]
|
||||
diags = [frozenset((order[0], order[j])) for j in range(2, d - 1)]
|
||||
if any(dg in Erest for dg in diags):
|
||||
continue # duplicate-edge: invalid retriangulation
|
||||
fan = [(order[0], order[j], order[j + 1]) for j in range(1, d - 1)]
|
||||
yield rest + fan
|
||||
|
||||
|
||||
def main():
|
||||
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
rng = np.random.default_rng(seed)
|
||||
irreducible_only = "--irr" in sys.argv
|
||||
|
||||
tested = 0
|
||||
has_good = 0
|
||||
fail_examples = []
|
||||
|
||||
for _ in range(4000):
|
||||
n = int(rng.integers(4, 7)); k = int(rng.integers(1, 4))
|
||||
faces = disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 14:
|
||||
continue
|
||||
deg = Counter()
|
||||
for f in faces:
|
||||
for x in f:
|
||||
if x >= n:
|
||||
deg[x] += 1
|
||||
if irreducible_only and (len(deg) < k or any(deg[x] < 4 for x in deg)):
|
||||
continue
|
||||
base = phi_size(faces, n)
|
||||
if base is None:
|
||||
continue
|
||||
best = None
|
||||
for v in [x for x in deg]:
|
||||
for D2 in removals(faces, v, n):
|
||||
s = phi_size(D2, n)
|
||||
if s is None:
|
||||
continue
|
||||
if best is None or s < best:
|
||||
best = s
|
||||
if best is None:
|
||||
continue
|
||||
tested += 1
|
||||
if best <= base:
|
||||
has_good += 1
|
||||
elif len(fail_examples) < 6:
|
||||
fail_examples.append((n, k, base, best, sorted(deg.values())))
|
||||
|
||||
tag = "irreducible" if irreducible_only else "all"
|
||||
print(f"disks tested ({tag}, k>=1): {tested}")
|
||||
print(f" have a Phi-non-increasing removal: {has_good}/{tested} "
|
||||
f"({100*has_good/max(tested,1):.1f}%)")
|
||||
if fail_examples:
|
||||
print(" FAILURES (every removal raised Phi) "
|
||||
"(n,k,|Phi(D)|,best|Phi(D-v)|,int-degs):")
|
||||
for e in fail_examples:
|
||||
print(f" {e}")
|
||||
else:
|
||||
print(" no failure: every disk had a non-increasing removal "
|
||||
"=> induction strategy A is viable.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Search for a UNIVERSAL Heawood boundary sequence for a tire graph.
|
||||
|
||||
Fix an outer boundary cycle B_out of length n (the interface at which a tire
|
||||
glues to its parent). Each way of filling the annulus -- an inner boundary of
|
||||
size m together with a spoke triangulation ("inner graph") -- gives a tire whose
|
||||
annular faces induce a set of realisable outer Heawood sequences
|
||||
|
||||
R_out(tire) = { (lambda*(v0), ..., lambda*(v_{n-1})) : lambda in {+1,-1}^F }
|
||||
⊆ {0,1,-1}^n .
|
||||
|
||||
A *universal sequence* for B_out is one realisable for EVERY inner graph, i.e. a
|
||||
member of the intersection ∩_tire R_out(tire). If a universal sequence existed,
|
||||
a parent could always present its negation and glue to any child regardless of
|
||||
the child's interior.
|
||||
|
||||
Note: chords of the inner outerplanar graph O lie inside B_in and bound no
|
||||
annular face, so they do not change R_out -- only (n, m, spoke-path) do. And
|
||||
intersecting over a SUBFAMILY of inner graphs can only OVERestimate the true
|
||||
intersection, so finding the intersection empty over simple-cycle inner fills is
|
||||
already conclusive that NO universal sequence exists.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from itertools import combinations, product
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def lattice_paths(n_outer, m_inner):
|
||||
"""All spoke triangulations: strings with n_outer 'O' moves, m_inner 'I'."""
|
||||
N = n_outer + m_inner
|
||||
for opos in combinations(range(N), n_outer):
|
||||
opos = set(opos)
|
||||
yield "".join("O" if i in opos else "I" for i in range(N))
|
||||
|
||||
|
||||
def annular_faces(n, m, path):
|
||||
"""Faces (triangles) of the annulus between outer n-cycle (0..n-1) and inner
|
||||
m-cycle (n..n+m-1) under the spoke path. Starts at spoke (outer0, inner0)."""
|
||||
faces = []
|
||||
i = j = 0
|
||||
for mv in path:
|
||||
if mv == "O":
|
||||
faces.append((i % n, (i + 1) % n, n + (j % m)))
|
||||
i += 1
|
||||
else:
|
||||
faces.append((i % n, n + (j % m), n + ((j + 1) % m)))
|
||||
j += 1
|
||||
return faces
|
||||
|
||||
|
||||
def fan_faces(n):
|
||||
"""m = 1 degenerate inner boundary: a wheel/fan, center = vertex n."""
|
||||
return [(i, (i + 1) % n, n) for i in range(n)]
|
||||
|
||||
|
||||
def realisable_outer(n, faces):
|
||||
"""Set of outer Heawood sequences over all +/-1 face labellings."""
|
||||
F = len(faces)
|
||||
A = np.zeros((n, F), dtype=np.int64) # outer-vertex x face incidence
|
||||
for f, tri in enumerate(faces):
|
||||
for v in tri:
|
||||
if v < n:
|
||||
A[v, f] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
vals = (labs @ A.T) % 3
|
||||
# display residues in {0, 1, -1}: 2 -> -1
|
||||
vals = np.where(vals == 2, -1, vals)
|
||||
return set(tuple(int(x) for x in row) for row in np.unique(vals, axis=0))
|
||||
|
||||
|
||||
def tires_for(n, m_max, fcap):
|
||||
"""Yield (label, faces) for inner fills of an n-outer tire."""
|
||||
yield (f"m=1 fan", fan_faces(n))
|
||||
for m in range(2, m_max + 1):
|
||||
if n + m > fcap:
|
||||
continue
|
||||
for path in lattice_paths(n, m):
|
||||
yield (f"m={m} {path}", annular_faces(n, m, path))
|
||||
|
||||
|
||||
def run(n, m_max=7, fcap=13):
|
||||
inter = None
|
||||
n_tires = 0
|
||||
min_set = (10**9, None)
|
||||
shrink_trace = []
|
||||
for label, faces in tires_for(n, m_max, fcap):
|
||||
R = realisable_outer(n, faces)
|
||||
n_tires += 1
|
||||
if len(R) < min_set[0]:
|
||||
min_set = (len(R), label)
|
||||
if inter is None:
|
||||
inter = set(R)
|
||||
else:
|
||||
before = len(inter)
|
||||
inter &= R
|
||||
if len(inter) < before:
|
||||
shrink_trace.append((n_tires, label, len(inter)))
|
||||
if not inter:
|
||||
break
|
||||
print(f"n={n}: {n_tires} tires tried, "
|
||||
f"smallest single R_out = {min_set[0]} ({min_set[1]})")
|
||||
if inter:
|
||||
print(f" UNIVERSAL sequences found: {len(inter)}")
|
||||
for s in sorted(inter)[:12]:
|
||||
print(f" {s}")
|
||||
else:
|
||||
print(f" NO universal sequence: intersection emptied after "
|
||||
f"{n_tires} tires")
|
||||
print(" intersection size as tires were added (last few shrinks):")
|
||||
for t in shrink_trace[-6:]:
|
||||
print(f" after tire {t[0]:4d} ({t[1]}): |∩| = {t[2]}")
|
||||
return bool(inter)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
ns = [int(sys.argv[1])]
|
||||
else:
|
||||
ns = [3, 4, 5, 6]
|
||||
print("Searching for universal Heawood boundary sequences\n")
|
||||
for n in ns:
|
||||
run(n)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Transfer operator for the Heawood program, in the cleanest self-similar setting:
|
||||
a chain of annular tires with n_out = n_in = n. Each tire's labelling map sends
|
||||
+/-1 face labels to (outer sequence, inner sequence). Gluing a child below means
|
||||
the parent's inner sequence must negate (mod 3) the child's achievable outer
|
||||
sequence. So the achievable outer-interface set propagates UP the chain by
|
||||
|
||||
Phi(parent) = { outer(lambda) : lambda in {+-1}^F,
|
||||
inner(lambda) in -Phi(child) }.
|
||||
|
||||
This is a monotone set-operator on subsets of (Z/3)^n. Iterating it models a
|
||||
deepening nested chain; we look for a FIXED POINT (absorbing set) and test which
|
||||
candidate self-similar invariants the limit satisfies:
|
||||
* non-empty
|
||||
* closed under the global sign flip s -> -s
|
||||
* local marginals: does every position attain all of {0,1,-1}?
|
||||
* is it an affine GF(3) subspace? (we expect NO -- R_T is a zonotope)
|
||||
* does a linear/parity constraint cut it out?
|
||||
Sequences are stored mod 3 in {0,1,2}; printed in {0,1,-1} (2 -> -1).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def annular_tire(n_out, n_in, path):
|
||||
"""Faces between outer cycle 0..n_out-1 and inner cycle n_out..n_out+n_in-1."""
|
||||
faces = []
|
||||
i = j = 0
|
||||
for mv in path:
|
||||
if mv == "O":
|
||||
faces.append((i % n_out, (i + 1) % n_out, n_out + (j % n_in)))
|
||||
i += 1
|
||||
else:
|
||||
faces.append((i % n_out, n_out + (j % n_in), n_out + ((j + 1) % n_in)))
|
||||
j += 1
|
||||
return faces
|
||||
|
||||
|
||||
def labelling_pairs(n_out, n_in, faces):
|
||||
"""All (outer_seq, inner_seq) over lambda in {+1,-1}^F, as Z/3 tuples."""
|
||||
F = len(faces)
|
||||
Ao = np.zeros((n_out, F), dtype=np.int64)
|
||||
Ai = np.zeros((n_in, F), dtype=np.int64)
|
||||
for f, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v < n_out:
|
||||
Ao[v, f] = 1
|
||||
else:
|
||||
Ai[v - n_out, f] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
outer = (labs @ Ao.T) % 3
|
||||
inner = (labs @ Ai.T) % 3
|
||||
return [(tuple(o), tuple(i)) for o, i in zip(outer.tolist(), inner.tolist())]
|
||||
|
||||
|
||||
def make_operator(pairs):
|
||||
def op(phi_child):
|
||||
neg = {tuple((3 - x) % 3 for x in s) for s in phi_child}
|
||||
return {o for (o, inn) in pairs if inn in neg}
|
||||
return op
|
||||
|
||||
|
||||
def iterate_to_fixed(op, start, max_iter=50):
|
||||
phi = frozenset(start)
|
||||
seen = [phi]
|
||||
for _ in range(max_iter):
|
||||
nxt = frozenset(op(phi))
|
||||
if nxt == phi:
|
||||
return phi, "fixed", len(seen)
|
||||
if nxt in seen:
|
||||
return nxt, "cycle", len(seen)
|
||||
phi = nxt
|
||||
seen.append(phi)
|
||||
return phi, "no-converge", len(seen)
|
||||
|
||||
|
||||
# ----------------- invariant tests -------------------------------------------
|
||||
def disp(s):
|
||||
return tuple(-1 if x == 2 else x for x in s)
|
||||
|
||||
|
||||
def gf3_rank(rows):
|
||||
M = [[x % 3 for x in r] for r in rows]
|
||||
if not M:
|
||||
return 0
|
||||
nc = len(M[0]); r = 0
|
||||
for c in range(nc):
|
||||
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
|
||||
if piv is None:
|
||||
continue
|
||||
M[r], M[piv] = M[piv], M[r]
|
||||
inv = M[r][c] % 3 # 1->1, 2->2 are self-inverse mod 3
|
||||
M[r] = [(x * inv) % 3 for x in M[r]]
|
||||
for i in range(len(M)):
|
||||
if i != r and M[i][c] % 3:
|
||||
f = M[i][c] % 3
|
||||
M[i] = [(M[i][k] - f * M[r][k]) % 3 for k in range(nc)]
|
||||
r += 1
|
||||
if r == len(M):
|
||||
break
|
||||
return r
|
||||
|
||||
|
||||
def is_affine(S):
|
||||
S = list(S)
|
||||
if len(S) <= 1:
|
||||
return True
|
||||
s0 = S[0]
|
||||
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
|
||||
return len(S) == 3 ** gf3_rank(D)
|
||||
|
||||
|
||||
def marginals_full(S, n):
|
||||
return all({s[i] for s in S} == {0, 1, 2} for i in range(n))
|
||||
|
||||
|
||||
def sign_closed(S):
|
||||
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
|
||||
|
||||
|
||||
def linear_constraints(S, n):
|
||||
"""Dimension of the space of linear forms vanishing on S-s0 (codim of hull)."""
|
||||
S = list(S)
|
||||
if len(S) <= 1:
|
||||
return n
|
||||
s0 = S[0]
|
||||
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
|
||||
return n - gf3_rank(D)
|
||||
|
||||
|
||||
def analyse(tag, S, n):
|
||||
print(f" [{tag}] |Phi|={len(S)} of 3^{n}={3**n} "
|
||||
f"sign-closed={sign_closed(S)} marginals-full={marginals_full(S,n)} "
|
||||
f"affine={is_affine(S)} hull-codim={linear_constraints(S,n)}")
|
||||
|
||||
|
||||
def run(n, paths=None):
|
||||
if paths is None:
|
||||
# a few distinct same-n annular triangulations
|
||||
paths = ["OI" * n, "O" * n + "I" * n, ("OOI" * n)[:2 * n]]
|
||||
paths = [p for p in paths if p.count("O") == n and p.count("I") == n]
|
||||
print(f"=== n={n} ===")
|
||||
full = set(product((0, 1, 2), repeat=n))
|
||||
for path in paths:
|
||||
faces = annular_tire(n, n, path)
|
||||
pairs = labelling_pairs(n, n, faces)
|
||||
op = make_operator(pairs)
|
||||
single = set(o for (o, _) in pairs) # leaf: full single-tire outer set
|
||||
fixed, how, steps = iterate_to_fixed(op, single)
|
||||
# also iterate from the universal start (all sequences allowed below)
|
||||
fixed2, how2, _ = iterate_to_fixed(op, full)
|
||||
print(f" path={path}: single-tire |outer|={len(single)}; "
|
||||
f"iterate->{how} in {steps} steps; "
|
||||
f"same-limit-from-full={fixed==fixed2}")
|
||||
analyse("limit", fixed, n)
|
||||
sample = sorted(disp(s) for s in fixed)[:8]
|
||||
print(f" sample of limit set: {sample}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6]
|
||||
print("Transfer-operator fixed points on same-n annular tire chains\n")
|
||||
for n in ns:
|
||||
run(n)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Option 2: a direct "strong transversal" for |Phi(D)| >= 2^(n-2).
|
||||
|
||||
In the Boolean reformulation, feasible x in {0,1}^F satisfy |x|_w ≡ -deg(w) at
|
||||
interior w, and the boundary sequence is (deg(v)+|x|_v mod 3)_{v in C}. A STRONG
|
||||
TRANSVERSAL is a set A of n-2 faces such that
|
||||
(i) every assignment x_A in {0,1}^A extends to a feasible x,
|
||||
(ii) the boundary sequence is single-valued in x_A (all feasible completions of
|
||||
a given x_A give the same boundary), and
|
||||
(iii) the map x_A -> boundary is injective.
|
||||
If such A exists, the 2^(n-2) assignments give 2^(n-2) distinct boundary sequences,
|
||||
proving the bound CONSTRUCTIVELY. We test whether a strong transversal exists.
|
||||
|
||||
Heuristic worry: over GF(3) the internal completion freedom (dim k) exceeds the
|
||||
boundary-invisible freedom (dim k-1), so (ii) may fail. Test settles it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from itertools import combinations, product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
def disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.18 * rng.random(n)
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
if k:
|
||||
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
|
||||
else:
|
||||
pts = bpts
|
||||
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
from collections import Counter
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def feasible_table(faces, n):
|
||||
"""Return list of (x tuple, boundary tuple) over feasible x in {0,1}^F."""
|
||||
interior = sorted(set(v for f in faces for v in f if v >= n))
|
||||
F = len(faces)
|
||||
if F > 14:
|
||||
return None
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
deg = np.zeros(n, dtype=np.int64)
|
||||
iidx = {w: r for r, w in enumerate(interior)}
|
||||
degw = np.zeros(len(interior), dtype=np.int64)
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iidx[v], j] = 1; degw[iidx[v]] += 1
|
||||
else:
|
||||
Cinc[v, j] = 1; deg[v] += 1
|
||||
xs = np.array(list(product((0, 1), repeat=F)), dtype=np.int64)
|
||||
if len(interior):
|
||||
ok = np.all((xs @ Bint.T - (-degw)) % 3 == 0, axis=1)
|
||||
xs = xs[ok]
|
||||
if xs.shape[0] == 0:
|
||||
return []
|
||||
bnd = (deg + xs @ Cinc.T) % 3
|
||||
return list(zip(map(tuple, xs.tolist()), map(tuple, bnd.tolist())))
|
||||
|
||||
|
||||
def has_strong_transversal(table, F, n):
|
||||
target = 2 ** (n - 2)
|
||||
for A in combinations(range(F), n - 2):
|
||||
groups = defaultdict(set)
|
||||
for x, b in table:
|
||||
groups[tuple(x[i] for i in A)].add(b)
|
||||
if len(groups) != target:
|
||||
continue # not every x_A feasible
|
||||
if any(len(bs) != 1 for bs in groups.values()):
|
||||
continue # not single-valued (cond ii)
|
||||
reps = [next(iter(bs)) for bs in groups.values()]
|
||||
if len(set(reps)) == target: # injective (cond iii)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
rng = np.random.default_rng(seed)
|
||||
tested = 0; have = 0; fails = []
|
||||
for _ in range(3000):
|
||||
n = int(rng.integers(4, 6)); k = int(rng.integers(1, 3))
|
||||
faces = disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 12:
|
||||
continue
|
||||
tbl = feasible_table(faces, n)
|
||||
if not tbl:
|
||||
continue
|
||||
tested += 1
|
||||
if has_strong_transversal(tbl, len(faces), n):
|
||||
have += 1
|
||||
elif len(fails) < 5:
|
||||
fails.append((n, k, len(faces)))
|
||||
print(f"disks tested (k>=1): {tested}")
|
||||
print(f" have a STRONG transversal (size n-2, single-valued, injective): "
|
||||
f"{have}/{tested} ({100*have/max(tested,1):.1f}%)")
|
||||
if fails:
|
||||
print(f" failures (n,k,F): {fails}")
|
||||
if have == tested and tested:
|
||||
print(" => strong transversals always exist: constructive proof viable.")
|
||||
elif have == 0:
|
||||
print(" => strong transversals NEVER exist: this clean form of option 2 is dead.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Pin the extremal irreducible disk.
|
||||
|
||||
The wheel W_n: boundary n-cycle + one center, faces (i,i+1,c). The center is the
|
||||
only interior vertex (degree n), constraint sum_i lambda_i ≡ 0 (mod 3), and the
|
||||
boundary value is the cyclic adjacent sum sigma_i = lambda_{i-1}+lambda_i (mod 3).
|
||||
So
|
||||
|
||||
|Phi(W_n)| = #{ (lambda_{i-1}+lambda_i)_i mod 3 : lambda in {+-1}^n, sum ≡ 0 } .
|
||||
|
||||
We (1) compute |Phi(W_n)| exactly for a range of n and look for a formula, and
|
||||
(2) run a thorough irreducible-disk search to check whether the wheel is actually
|
||||
the MINIMISER over irreducible disks (and dump the minimiser's structure).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
np.seterr(all="ignore")
|
||||
|
||||
|
||||
# ---------------- exact wheel value ------------------------------------------
|
||||
def wheel_phi_size(n):
|
||||
S = set()
|
||||
cnt = 0
|
||||
for lam in product((1, -1), repeat=n):
|
||||
if sum(lam) % 3 != 0:
|
||||
continue
|
||||
cnt += 1
|
||||
sig = tuple((lam[i - 1] + lam[i]) % 3 for i in range(n))
|
||||
S.add(sig)
|
||||
return len(S), cnt # distinct boundary seqs, feasible labellings
|
||||
|
||||
|
||||
# ---------------- general disk Phi -------------------------------------------
|
||||
def disk(n, k, rng):
|
||||
ang = 2 * np.pi * np.arange(n) / n
|
||||
rad = 1.0 + 0.18 * rng.random(n)
|
||||
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
|
||||
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
|
||||
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
|
||||
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
|
||||
|
||||
|
||||
def valid(faces, n, k):
|
||||
if len(faces) != 2 * k + n - 2:
|
||||
return False
|
||||
ec = Counter()
|
||||
for a, b, c in faces:
|
||||
for e in ((a, b), (b, c), (a, c)):
|
||||
ec[frozenset(e)] += 1
|
||||
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
|
||||
|
||||
|
||||
def phi_size(faces, n):
|
||||
interior = sorted(set(v for f in faces for v in f if v >= n))
|
||||
F = len(faces)
|
||||
if F > 18:
|
||||
return None
|
||||
Bint = np.zeros((len(interior), F), dtype=np.int64)
|
||||
Cinc = np.zeros((n, F), dtype=np.int64)
|
||||
iidx = {w: r for r, w in enumerate(interior)}
|
||||
for j, (a, b, c) in enumerate(faces):
|
||||
for v in (a, b, c):
|
||||
if v >= n:
|
||||
Bint[iidx[v], j] = 1
|
||||
else:
|
||||
Cinc[v, j] = 1
|
||||
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
|
||||
if interior:
|
||||
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
|
||||
if labs.shape[0] == 0:
|
||||
return 0
|
||||
return len(set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0))))
|
||||
|
||||
|
||||
def min_degree_ok(faces, n, k):
|
||||
deg = Counter()
|
||||
for f in faces:
|
||||
for v in f:
|
||||
if v >= n:
|
||||
deg[v] += 1
|
||||
return len(deg) == k and all(deg[v] >= 4 for v in deg)
|
||||
|
||||
|
||||
def single_deg_disk(n, d):
|
||||
"""One interior vertex v=n of degree d: fan over boundary 0..d-1, the rest of
|
||||
the n-gon polygon-triangulated from vertex 0 (so v stays degree d, k=1)."""
|
||||
v = n
|
||||
faces = [(i, i + 1, v) for i in range(d - 1)]
|
||||
poly = [0] + list(range(n - 1, d - 2, -1)) + [v]
|
||||
for j in range(1, len(poly) - 1):
|
||||
faces.append((0, poly[j], poly[j + 1]))
|
||||
return faces
|
||||
|
||||
|
||||
def degree_sweep():
|
||||
print("\n|Phi| of a single degree-d interior vertex (rest at the floor):\n")
|
||||
print(" ratio |Phi|/2^(n-2) by center degree d -- MINIMUM marks the extremal disk")
|
||||
for n in (6, 7, 8):
|
||||
row = []
|
||||
for d in range(4, n + 1):
|
||||
f = single_deg_disk(n, d)
|
||||
row.append(f"d={d}:{phi_size(f, n)/2**(n-2):.4f}")
|
||||
print(f" n={n} (floor {2**(n-2)}): " + " ".join(row))
|
||||
print(" => min ratio 5/4 at d=4,5 (extremal); rises with d to ~4/3 at the wheel.")
|
||||
|
||||
|
||||
def main():
|
||||
print("Exact |Phi(W_n)| and candidate formula:\n")
|
||||
print(" n |Phi(W_n)| feasible-labellings ratio-to-2^(n-2)")
|
||||
vals = {}
|
||||
for n in range(3, 15):
|
||||
s, cnt = wheel_phi_size(n)
|
||||
vals[n] = s
|
||||
print(f" {n:2d} {s:8d} {cnt:14d} {s / 2**(n-2):.4f}")
|
||||
# differences / ratios to spot a pattern
|
||||
print("\n consecutive ratios |Phi(W_{n+1})| / |Phi(W_n)|:")
|
||||
for n in range(3, 14):
|
||||
print(f" {n}->{n+1}: {vals[n+1]/vals[n]:.4f}")
|
||||
|
||||
print("\nThe actual irreducible minimiser (Delaunay search, dumping structure)\n")
|
||||
rng = np.random.default_rng(0)
|
||||
for n in (4, 5, 6, 7):
|
||||
best = 10**9; bestdeg = None; bestk = None
|
||||
for _ in range(12000):
|
||||
k = int(rng.integers(1, 4))
|
||||
faces = disk(n, k, rng)
|
||||
if not valid(faces, n, k) or len(faces) > 16:
|
||||
continue
|
||||
if not min_degree_ok(faces, n, k):
|
||||
continue
|
||||
s = phi_size(faces, n)
|
||||
if s and s < best:
|
||||
best = s
|
||||
deg = Counter()
|
||||
for f in faces:
|
||||
for v in f:
|
||||
if v >= n:
|
||||
deg[v] += 1
|
||||
bestdeg = sorted(deg.values()); bestk = k
|
||||
floor = 2 ** (n - 2)
|
||||
print(f" n={n}: min irreducible |Phi|={best} (k={bestk}, interior degrees "
|
||||
f"{bestdeg}) ratio to floor = {best/floor:.4f} "
|
||||
f"5*2^(n-4)={5*2**(n-4)} wheel={vals[n]}")
|
||||
degree_sweep()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
\relax
|
||||
\newlabel{lem:unstack}{{}{2}}
|
||||
\newlabel{lem:base}{{}{3}}
|
||||
\newlabel{prop:reduction}{{}{3}}
|
||||
\newlabel{conj:irreducible}{{}{3}}
|
||||
\gdef \@abspage@last{3}
|
||||
@@ -0,0 +1,300 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 17 JUN 2026 21:32
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
**boundary_restriction_structure.tex
|
||||
(./boundary_restriction_structure.tex
|
||||
LaTeX2e <2021-11-15> patch level 1
|
||||
L3 programming layer <2022-02-24>
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/article.cls
|
||||
Document Class: article 2021/10/04 v1.4n Standard LaTeX document class
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/size11.clo
|
||||
File: size11.clo 2021/10/04 v1.4n Standard LaTeX file (size option)
|
||||
)
|
||||
\c@part=\count185
|
||||
\c@section=\count186
|
||||
\c@subsection=\count187
|
||||
\c@subsubsection=\count188
|
||||
\c@paragraph=\count189
|
||||
\c@subparagraph=\count190
|
||||
\c@figure=\count191
|
||||
\c@table=\count192
|
||||
\abovecaptionskip=\skip47
|
||||
\belowcaptionskip=\skip48
|
||||
\bibindent=\dimen138
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
|
||||
Package: amsmath 2021/10/15 v2.17l AMS math features
|
||||
\@mathmargin=\skip49
|
||||
|
||||
For additional information on amsmath, use the `?' option.
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
|
||||
Package: amstext 2021/08/26 v2.01 AMS text
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
|
||||
File: amsgen.sty 1999/11/30 v2.0 generic functions
|
||||
\@emptytoks=\toks16
|
||||
\ex@=\dimen139
|
||||
))
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
|
||||
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
|
||||
\pmbraise@=\dimen140
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
|
||||
Package: amsopn 2021/08/26 v2.02 operator names
|
||||
)
|
||||
\inf@bad=\count193
|
||||
LaTeX Info: Redefining \frac on input line 234.
|
||||
\uproot@=\count194
|
||||
\leftroot@=\count195
|
||||
LaTeX Info: Redefining \overline on input line 399.
|
||||
\classnum@=\count196
|
||||
\DOTSCASE@=\count197
|
||||
LaTeX Info: Redefining \ldots on input line 496.
|
||||
LaTeX Info: Redefining \dots on input line 499.
|
||||
LaTeX Info: Redefining \cdots on input line 620.
|
||||
\Mathstrutbox@=\box50
|
||||
\strutbox@=\box51
|
||||
\big@size=\dimen141
|
||||
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
|
||||
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
|
||||
\macc@depth=\count198
|
||||
\c@MaxMatrixCols=\count199
|
||||
\dotsspace@=\muskip16
|
||||
\c@parentequation=\count266
|
||||
\dspbrk@lvl=\count267
|
||||
\tag@help=\toks17
|
||||
\row@=\count268
|
||||
\column@=\count269
|
||||
\maxfields@=\count270
|
||||
\andhelp@=\toks18
|
||||
\eqnshift@=\dimen142
|
||||
\alignsep@=\dimen143
|
||||
\tagshift@=\dimen144
|
||||
\tagwidth@=\dimen145
|
||||
\totwidth@=\dimen146
|
||||
\lineht@=\dimen147
|
||||
\@envbody=\toks19
|
||||
\multlinegap=\skip50
|
||||
\multlinetaggap=\skip51
|
||||
\mathdisplay@stack=\toks20
|
||||
LaTeX Info: Redefining \[ on input line 2938.
|
||||
LaTeX Info: Redefining \] on input line 2939.
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
|
||||
Package: amssymb 2013/01/14 v3.01 AMS font symbols
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
|
||||
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
|
||||
\symAMSa=\mathgroup4
|
||||
\symAMSb=\mathgroup5
|
||||
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
|
||||
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
|
||||
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
|
||||
))
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsthm.sty
|
||||
Package: amsthm 2020/05/29 v2.20.6
|
||||
\thm@style=\toks21
|
||||
\thm@bodyfont=\toks22
|
||||
\thm@headfont=\toks23
|
||||
\thm@notefont=\toks24
|
||||
\thm@headpunct=\toks25
|
||||
\thm@preskip=\skip52
|
||||
\thm@postskip=\skip53
|
||||
\thm@headsep=\skip54
|
||||
\dth@everypar=\toks26
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
|
||||
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
|
||||
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
|
||||
\KV@toks@=\toks27
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
|
||||
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
|
||||
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
|
||||
)
|
||||
Package graphics Info: Driver file: pdftex.def on input line 107.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
|
||||
))
|
||||
\Gin@req@height=\dimen148
|
||||
\Gin@req@width=\dimen149
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/geometry/geometry.sty
|
||||
Package: geometry 2020/01/02 v5.9 Page Geometry
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/ifvtex.sty
|
||||
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/iftex.sty
|
||||
Package: iftex 2022/02/03 v1.0f TeX engine tests
|
||||
))
|
||||
\Gm@cnth=\count271
|
||||
\Gm@cntv=\count272
|
||||
\c@Gm@tempcnt=\count273
|
||||
\Gm@bindingoffset=\dimen150
|
||||
\Gm@wd@mp=\dimen151
|
||||
\Gm@odd@mp=\dimen152
|
||||
\Gm@even@mp=\dimen153
|
||||
\Gm@layoutwidth=\dimen154
|
||||
\Gm@layoutheight=\dimen155
|
||||
\Gm@layouthoffset=\dimen156
|
||||
\Gm@layoutvoffset=\dimen157
|
||||
\Gm@dimlist=\toks28
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/booktabs/booktabs.sty
|
||||
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
|
||||
\heavyrulewidth=\dimen158
|
||||
\lightrulewidth=\dimen159
|
||||
\cmidrulewidth=\dimen160
|
||||
\belowrulesep=\dimen161
|
||||
\belowbottomsep=\dimen162
|
||||
\aboverulesep=\dimen163
|
||||
\abovetopsep=\dimen164
|
||||
\cmidrulesep=\dimen165
|
||||
\cmidrulekern=\dimen166
|
||||
\defaultaddspace=\dimen167
|
||||
\@cmidla=\count274
|
||||
\@cmidlb=\count275
|
||||
\@aboverulesep=\dimen168
|
||||
\@belowrulesep=\dimen169
|
||||
\@thisruleclass=\count276
|
||||
\@lastruleclass=\count277
|
||||
\@thisrulewidth=\dimen170
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
|
||||
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
|
||||
\l__color_backend_stack_int=\count278
|
||||
\l__pdf_internal_box=\box52
|
||||
)
|
||||
(./boundary_restriction_structure.aux)
|
||||
\openout1 = `boundary_restriction_structure.aux'.
|
||||
|
||||
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 19.
|
||||
LaTeX Font Info: ... okay on input line 19.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
|
||||
[Loading MPS to PDF converter (version 2006.09.02).]
|
||||
\scratchcounter=\count279
|
||||
\scratchdimen=\dimen171
|
||||
\scratchbox=\box53
|
||||
\nofMPsegments=\count280
|
||||
\nofMParguments=\count281
|
||||
\everyMPshowfont=\toks29
|
||||
\MPscratchCnt=\count282
|
||||
\MPscratchDim=\dimen172
|
||||
\MPnumerator=\count283
|
||||
\makeMPintoPDFobject=\count284
|
||||
\everyMPtoPDFconversion=\toks30
|
||||
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
|
||||
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
|
||||
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
|
||||
85.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
|
||||
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
|
||||
e
|
||||
))
|
||||
*geometry* driver: auto-detecting
|
||||
*geometry* detected driver: pdftex
|
||||
*geometry* verbose mode - [ preamble ] result:
|
||||
* driver: pdftex
|
||||
* paper: <default>
|
||||
* layout: <same size as paper>
|
||||
* layoutoffset:(h,v)=(0.0pt,0.0pt)
|
||||
* modes:
|
||||
* h-part:(L,W,R)=(72.26999pt, 469.75502pt, 72.26999pt)
|
||||
* v-part:(T,H,B)=(72.26999pt, 650.43001pt, 72.26999pt)
|
||||
* \paperwidth=614.295pt
|
||||
* \paperheight=794.96999pt
|
||||
* \textwidth=469.75502pt
|
||||
* \textheight=650.43001pt
|
||||
* \oddsidemargin=0.0pt
|
||||
* \evensidemargin=0.0pt
|
||||
* \topmargin=-37.0pt
|
||||
* \headheight=12.0pt
|
||||
* \headsep=25.0pt
|
||||
* \topskip=11.0pt
|
||||
* \footskip=30.0pt
|
||||
* \marginparwidth=59.0pt
|
||||
* \marginparsep=10.0pt
|
||||
* \columnsep=10.0pt
|
||||
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
|
||||
* \hoffset=0.0pt
|
||||
* \voffset=0.0pt
|
||||
* \mag=1000
|
||||
* \@twocolumnfalse
|
||||
* \@twosidefalse
|
||||
* \@mparswitchfalse
|
||||
* \@reversemarginfalse
|
||||
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
|
||||
|
||||
LaTeX Font Info: Trying to load font information for U+msa on input line 20.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
|
||||
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
|
||||
)
|
||||
LaTeX Font Info: Trying to load font information for U+msb on input line 20.
|
||||
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
|
||||
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
|
||||
) [1
|
||||
|
||||
{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] [2] [3]
|
||||
|
||||
(./boundary_restriction_structure.aux) )
|
||||
Here is how much of TeX's memory you used:
|
||||
3268 strings out of 478268
|
||||
48713 string characters out of 5846347
|
||||
350712 words of memory out of 5000000
|
||||
21451 multiletter control sequences out of 15000+600000
|
||||
481419 words of font info for 73 fonts, out of 8000000 for 9000
|
||||
1141 hyphenation exceptions out of 8191
|
||||
55i,8n,62p,247b,208s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf-dist/fon
|
||||
ts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/font
|
||||
s/type1/public/amsfonts/cm/cmbx12.pfb></usr/local/texlive/2022/texmf-dist/fonts
|
||||
/type1/public/amsfonts/cm/cmbxti10.pfb></usr/local/texlive/2022/texmf-dist/font
|
||||
s/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts
|
||||
/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/
|
||||
type1/public/amsfonts/cm/cmmi12.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
|
||||
ype1/public/amsfonts/cm/cmmi6.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
|
||||
e1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
|
||||
/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
|
||||
ublic/amsfonts/cm/cmr12.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
|
||||
lic/amsfonts/cm/cmr17.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
|
||||
c/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
|
||||
msfonts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
|
||||
fonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
|
||||
onts/cm/cmsy8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
|
||||
ts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
|
||||
s/cm/cmtt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
|
||||
/symbols/msbm10.pfb>
|
||||
Output written on boundary_restriction_structure.pdf (3 pages, 218332 bytes).
|
||||
PDF statistics:
|
||||
104 PDF objects out of 1000 (max. 8388607)
|
||||
62 compressed objects within 1 object stream
|
||||
0 named destinations out of 1000 (max. 500000)
|
||||
1 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
\documentclass[11pt]{article}
|
||||
\usepackage{amsmath,amssymb,amsthm}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{geometry}
|
||||
\usepackage{booktabs}
|
||||
\geometry{margin=1in}
|
||||
|
||||
\title{Heawood boundary restriction sets:\\
|
||||
zonotope structure and the $2^{n-2}$ constraint floor}
|
||||
\author{}
|
||||
\date{}
|
||||
|
||||
\newtheorem*{obs}{Observation}
|
||||
\newtheorem*{prop}{Proposition}
|
||||
\newtheorem*{conj}{Conjecture}
|
||||
\newtheorem*{lem}{Lemma}
|
||||
\newtheorem*{verify}{Empirical check}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
This note records the empirical structure of the Heawood boundary
|
||||
restriction sets studied in \texttt{paper.tex}, and a clean
|
||||
\emph{maximal-constraint} result. All claims below are backed by the
|
||||
experiments in \texttt{experiments/} (filenames given inline). Sequences
|
||||
live in $(\mathbb{Z}/3)^{\,\cdot}$, displayed in $\{0,1,-1\}$.
|
||||
|
||||
\section*{Setup}
|
||||
|
||||
Fix a triangulated disk $D$ with boundary cycle $C = (v_0,\dots,v_{n-1})$.
|
||||
A Heawood face-labelling is $\lambda : \{\text{faces of }D\} \to \{+1,-1\}$,
|
||||
with induced vertex value $\lambda^{*}(v) = \sum_{f \ni v}\lambda(f) \bmod 3$.
|
||||
The achievable outer set is
|
||||
\[
|
||||
\Phi(D) \;=\; \bigl\{\, (\lambda^{*}(v_0),\dots,\lambda^{*}(v_{n-1}))
|
||||
\;:\; \lambda \in \{+1,-1\}^{F(D)},\;
|
||||
\lambda^{*}(w) \equiv 0 \ \forall\ \text{interior } w \,\bigr\}.
|
||||
\]
|
||||
This is exactly the value the recursive transfer operator produces at
|
||||
$C$ (interior consistency $=$ all descendant gluings performed; boundary
|
||||
deferred). Crucially $\Phi(D)$ depends \emph{only} on the disk
|
||||
triangulation, not on any BFS/tire-tree labelling.
|
||||
|
||||
\section*{1. The restriction sets are zonotopes, not subspaces}
|
||||
|
||||
(\texttt{probe\_RK\_structure.py}.) Writing $\lambda = \mathbf{1}+b$ with
|
||||
$b \in \{0,1\}^F$, the labelling map is $\lambda \mapsto M\mathbf{1}+Mb
|
||||
\pmod 3$, a linear image of the Boolean cube ($M$ the face/vertex
|
||||
incidence matrix). Over $3655$ cluster restriction sets $R_{\mathsf K}$:
|
||||
none was an affine $\mathrm{GF}(3)$ subspace; the map is usually
|
||||
injective, so $|R_{\mathsf K}| = 2^{|F|}$ (a power of $2$ inside the
|
||||
column space of size $3^{\operatorname{rank} M}$); the nowhere-zero
|
||||
constraint $\lambda \neq 0$ shrank the set below the full linear image in
|
||||
\emph{every} case. The only surviving linear structure is
|
||||
$R_{\mathsf K} \subseteq \operatorname{col}(M)$ (cokernel relations such
|
||||
as $\sum_v \lambda^{*}(v) \equiv 0$). So $\Phi$ is a $\mathbb{Z}/3$
|
||||
zonotope: a projected cube, sign-closed but not closed under addition.
|
||||
|
||||
\section*{2. ``Richness'' is not a self-similar invariant}
|
||||
|
||||
(\texttt{transfer\_operator.py}, \texttt{branch\_invariant.py}.) In a
|
||||
homogeneous same-$n$ spoke-only chain the operator saturates: $\Phi$ has
|
||||
full single-position marginals (every interface vertex independently
|
||||
attains all of $\{0,1,-1\}$), and the alternating tire reaches the
|
||||
\emph{entire} space $3^n$. This is an artifact of non-shrinking annuli
|
||||
with no interior constraints. On genuine triangulations the marginal
|
||||
fullness holds for only ${\sim}8\%$ of regions: depth (not branching)
|
||||
shrinks $\Phi$, e.g.\ a region with $|C|=10$ realised only $|\Phi|=400$
|
||||
of $3^{10}\approx 59000$. Only non-emptiness and sign-closure survive,
|
||||
both of which are automatic / equivalent to $4$CT. Hence no abundance
|
||||
(counting) pigeonhole: a working invariant must tolerate \emph{small}
|
||||
$\Phi$.
|
||||
|
||||
\section*{3. The maximal-constraint floor}
|
||||
|
||||
(\texttt{maximally\_constrain.py}.) Minimising $|\Phi(D)|$ over disks with
|
||||
a fixed boundary $n$-cycle:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{ccccc}
|
||||
\toprule
|
||||
$n$ & $4$ & $5$ & $6$ & $7$\\
|
||||
\midrule
|
||||
$\min |\Phi|$ (search) & $4$ & $8$ & $16$ & $32$\\
|
||||
fan, $0$ interior vertices & $4$ & $8$ & $16$ & $32$\\
|
||||
$2^{\,n-2}$ & $4$ & $8$ & $16$ & $32$\\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
A search over $1700{+}$ \emph{validated} triangulated disks per $n$
|
||||
(boundary points in convex but non-cocircular position, random interior
|
||||
points, Delaunay; each checked to have $2k+n-2$ faces and all $n$
|
||||
boundary edges present), together with deep-stacked single-apex chains up
|
||||
to $8$ interior vertices, never beat $2^{n-2}$, and the interior-free
|
||||
triangulation already attains it. (Note: cocircular boundary points
|
||||
produce degenerate Delaunay outputs --- invalid disks missing a boundary
|
||||
edge --- which spuriously report sub-floor values; these are excluded by
|
||||
the validity check.) Counterintuitively, adding interior structure tends
|
||||
to \emph{enlarge} $\Phi$: e.g.\ on the $4$-cycle the central-apex wheel
|
||||
realises $5$ sequences against the fan's $4$, since each interior vertex
|
||||
contributes two faces but only one constraint. Thus:
|
||||
|
||||
\begin{obs}
|
||||
The interior-free triangulation already attains $2^{n-2}$, no search disk
|
||||
beats it, and deep nesting only approaches this value from above ---
|
||||
suggesting it is a floor, with a single trivial tire already maximally
|
||||
constraining. Whether $2^{n-2}$ is a genuine lower bound for \emph{all}
|
||||
disks is the Conjecture below; it is \emph{not} a proven theorem.
|
||||
\end{obs}
|
||||
|
||||
The achievability is transparent: in a fan from $v_0$,
|
||||
\[
|
||||
\sigma_1 = \lambda_1,\quad
|
||||
\sigma_i = \lambda_{i-1}+\lambda_i \ (1<i<n-1),\quad
|
||||
\sigma_{n-1} = \lambda_{n-2},\quad
|
||||
\sigma_0 = \textstyle\sum_j \lambda_j ,
|
||||
\]
|
||||
so $(\lambda_1,\dots,\lambda_{n-2})$ is recoverable from $\sigma$ and the
|
||||
map is injective onto $2^{n-2}$ sequences. The lower bound over
|
||||
\emph{all} disks is the substance:
|
||||
|
||||
\begin{conj}[Boundary degrees of freedom]
|
||||
For every triangulated disk $D$ with boundary $n$-cycle,
|
||||
$|\Phi(D)| \ge 2^{n-2}$. Equivalently, the $n-2$ binary degrees of
|
||||
freedom carried by the boundary-incident faces survive every interior
|
||||
Heawood constraint (which relates only interior-incident faces).
|
||||
\end{conj}
|
||||
|
||||
The minimal set is itself a sign-closed zonotope of size $2^{n-2}$, hull
|
||||
dimension $n-2$, not a $\mathrm{GF}(3)$ subspace --- the same fingerprint
|
||||
as $\S1$.
|
||||
|
||||
\section*{4. A proof programme for the lower bound}
|
||||
|
||||
The lower bound $|\Phi(D)| \ge 2^{n-2}$ reduces, by an exact
|
||||
$\Phi$-preserving reduction, to a single lemma about ``irreducible''
|
||||
disks. Two dead ends bound the search first: \emph{monotonicity is false}
|
||||
--- inserting a degree-$4$ interior vertex can shrink $|\Phi|$ ($6\to5$,
|
||||
$30\to28$; \texttt{monotonicity\_test.py}), so there is no reduce-to-base
|
||||
proof by ``adding vertices only grows $\Phi$''; and \emph{universal
|
||||
toggles are insufficient} --- a flip preserves feasibility for every
|
||||
labelling only if it touches no interior vertex (a \emph{boundary-only}
|
||||
face), and an irreducible disk can have none (the wheel has zero). What
|
||||
does work:
|
||||
|
||||
\begin{lem}[Un-stacking; degree-$3$ removal preserves $\Phi$]
|
||||
\label{lem:unstack}
|
||||
Let $v$ be a degree-$3$ interior vertex of $D$, with link triangle
|
||||
$abc$ and incident faces $(vab),(vbc),(vca)$. Its constraint
|
||||
$\lambda_{vab}+\lambda_{vbc}+\lambda_{vca}\equiv 0 \pmod 3$ over
|
||||
$\{+1,-1\}$ forces the three to a common value $s$, so each of $a,b,c$
|
||||
receives $2s\equiv -s$ from $v$'s star. Let $D'$ delete $v$ and restore
|
||||
$abc$ as one face. Then setting that face to $-s$ reproduces the
|
||||
contribution $-s$ at $a,b,c$, and $s\mapsto -s$ is a bijection on
|
||||
$\{+1,-1\}$. Hence the map is a bijection between feasible labellings of
|
||||
$D$ and of $D'$ preserving every boundary value and interior constraint,
|
||||
so
|
||||
\[
|
||||
\Phi(D) = \Phi(D'), \qquad k(D') = k(D)-1 .
|
||||
\]
|
||||
\end{lem}
|
||||
|
||||
\begin{verify}\textnormal{(\texttt{monotonicity\_test.py})} Degree-$3$
|
||||
insertion gave exact equality in $8884/8884$ trials.\end{verify}
|
||||
|
||||
\begin{lem}[Base case; ear-peeling]
|
||||
\label{lem:base}
|
||||
If $D$ has no interior vertices ($k=0$) then $|\Phi(D)| = 2^{n-2}$. A
|
||||
polygon triangulation has an \emph{ear} $(v_{i-1},v_i,v_{i+1})$ with
|
||||
$v_i$ of face-degree $1$, so $\sigma_{v_i}=\lambda_{\mathrm{ear}}$ reads
|
||||
the ear label directly; remove it and induct on the $(n-1)$-gon. The
|
||||
boundary map is injective, giving $2^{n-2}$.
|
||||
\end{lem}
|
||||
|
||||
\begin{prop}[Reduction to the irreducible case]
|
||||
\label{prop:reduction}
|
||||
Iterating Lemma~\ref{lem:unstack} terminates ($k$ strictly decreases) at
|
||||
a residue $D^{\ast}$ with no degree-$3$ interior vertex and the same
|
||||
$n$, and $\Phi(D)=\Phi(D^{\ast})$. The residue is either $k=0$, where
|
||||
$|\Phi|=2^{n-2}$ by Lemma~\ref{lem:base}, or \emph{irreducible}: $k\ge1$
|
||||
with every interior vertex of degree $\ge 4$. Hence
|
||||
\[
|
||||
|\Phi(D)| \ge 2^{n-2}
|
||||
\quad\Longleftarrow\quad
|
||||
|\Phi(D^{\ast})| \ge 2^{n-2}\ \text{for every irreducible } D^{\ast}.
|
||||
\]
|
||||
\end{prop}
|
||||
|
||||
\begin{conj}[Irreducible lemma --- the remaining content]
|
||||
\label{conj:irreducible}
|
||||
Every irreducible disk satisfies $|\Phi| \ge 2^{n-2}$; in fact
|
||||
$|\Phi| \ge \tfrac54\cdot 2^{n-2} = 5\cdot 2^{n-4}$.
|
||||
\end{conj}
|
||||
|
||||
\begin{verify}\textnormal{(\texttt{irreducible\_floor.py},
|
||||
\texttt{wheel\_extremal.py})} Over $10^4{+}$ irreducible disks
|
||||
($n=4,5,6$) there were $0$ floor violations and none sat on the floor.
|
||||
The bound $\tfrac54\cdot 2^{n-2}$ is \emph{tight}, attained by a single
|
||||
\textbf{minimal-degree} interior vertex (degree $4$ or $5$, which tie):
|
||||
the ratio $|\Phi|/2^{n-2}$ rises monotonically with the interior vertex's
|
||||
degree, $\tfrac54$ at $d\in\{4,5\}$, $\tfrac{21}{16}$ at $d\in\{6,7\}$,
|
||||
$\ldots$, up to $\tfrac43$ at the wheel $d=n$, where
|
||||
$|\Phi(W_n)|=\lfloor 2^n/3\rfloor$ exactly. So a proof of
|
||||
Conjecture~\ref{conj:irreducible} should be stress-tested against the
|
||||
degree-$4$ patch, the tight case --- \emph{not} the wheel.\end{verify}
|
||||
|
||||
\noindent
|
||||
\emph{Status.} Lemmas~\ref{lem:unstack}--\ref{lem:base} and
|
||||
Proposition~\ref{prop:reduction} are proofs; they settle every disk that
|
||||
un-stacks to $k=0$ (the entire Apollonian class). The whole open content
|
||||
is Conjecture~\ref{conj:irreducible}, with guaranteed $25\%$ slack and a
|
||||
single explicit extremal disk.
|
||||
|
||||
\section*{Consequence for the pigeonhole}
|
||||
|
||||
Even a maximally-constraining child still presents $2^{n-2}$ outer
|
||||
options --- exponential in the interface length $n$. So the gluing
|
||||
problem has the least slack at \emph{short} interfaces ($n=4$ leaves $4$
|
||||
options, $n=3$ leaves $2$), and is easy at long ones. The crux of the
|
||||
Heawood programme therefore lives entirely at short level cycles, exactly
|
||||
where the medial programme's $N(k)$ bound concentrates.
|
||||
|
||||
\medskip
|
||||
\noindent\emph{Meta-remark.} Because $4$CT holds, every actual
|
||||
triangulation glues, so no experiment can exhibit an obstruction (pair or
|
||||
chain). The experiments measure \emph{structure} (zonotope type,
|
||||
constraint floor), not proof difficulty; the difficulty is localised, not
|
||||
removed.
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,4 @@
|
||||
\relax
|
||||
\@writefile{toc}{\contentsline {paragraph}{Inner.}{2}{}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {paragraph}{Outer.}{2}{}\protected@file@percent }
|
||||
\gdef \@abspage@last{2}
|
||||
@@ -0,0 +1,296 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 18 JUN 2026 23:27
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
**double_contraction_reductio.tex
|
||||
(./double_contraction_reductio.tex
|
||||
LaTeX2e <2021-11-15> patch level 1
|
||||
L3 programming layer <2022-02-24>
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/article.cls
|
||||
Document Class: article 2021/10/04 v1.4n Standard LaTeX document class
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/size11.clo
|
||||
File: size11.clo 2021/10/04 v1.4n Standard LaTeX file (size option)
|
||||
)
|
||||
\c@part=\count185
|
||||
\c@section=\count186
|
||||
\c@subsection=\count187
|
||||
\c@subsubsection=\count188
|
||||
\c@paragraph=\count189
|
||||
\c@subparagraph=\count190
|
||||
\c@figure=\count191
|
||||
\c@table=\count192
|
||||
\abovecaptionskip=\skip47
|
||||
\belowcaptionskip=\skip48
|
||||
\bibindent=\dimen138
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
|
||||
Package: amsmath 2021/10/15 v2.17l AMS math features
|
||||
\@mathmargin=\skip49
|
||||
|
||||
For additional information on amsmath, use the `?' option.
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
|
||||
Package: amstext 2021/08/26 v2.01 AMS text
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
|
||||
File: amsgen.sty 1999/11/30 v2.0 generic functions
|
||||
\@emptytoks=\toks16
|
||||
\ex@=\dimen139
|
||||
))
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
|
||||
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
|
||||
\pmbraise@=\dimen140
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
|
||||
Package: amsopn 2021/08/26 v2.02 operator names
|
||||
)
|
||||
\inf@bad=\count193
|
||||
LaTeX Info: Redefining \frac on input line 234.
|
||||
\uproot@=\count194
|
||||
\leftroot@=\count195
|
||||
LaTeX Info: Redefining \overline on input line 399.
|
||||
\classnum@=\count196
|
||||
\DOTSCASE@=\count197
|
||||
LaTeX Info: Redefining \ldots on input line 496.
|
||||
LaTeX Info: Redefining \dots on input line 499.
|
||||
LaTeX Info: Redefining \cdots on input line 620.
|
||||
\Mathstrutbox@=\box50
|
||||
\strutbox@=\box51
|
||||
\big@size=\dimen141
|
||||
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
|
||||
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
|
||||
\macc@depth=\count198
|
||||
\c@MaxMatrixCols=\count199
|
||||
\dotsspace@=\muskip16
|
||||
\c@parentequation=\count266
|
||||
\dspbrk@lvl=\count267
|
||||
\tag@help=\toks17
|
||||
\row@=\count268
|
||||
\column@=\count269
|
||||
\maxfields@=\count270
|
||||
\andhelp@=\toks18
|
||||
\eqnshift@=\dimen142
|
||||
\alignsep@=\dimen143
|
||||
\tagshift@=\dimen144
|
||||
\tagwidth@=\dimen145
|
||||
\totwidth@=\dimen146
|
||||
\lineht@=\dimen147
|
||||
\@envbody=\toks19
|
||||
\multlinegap=\skip50
|
||||
\multlinetaggap=\skip51
|
||||
\mathdisplay@stack=\toks20
|
||||
LaTeX Info: Redefining \[ on input line 2938.
|
||||
LaTeX Info: Redefining \] on input line 2939.
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
|
||||
Package: amssymb 2013/01/14 v3.01 AMS font symbols
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
|
||||
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
|
||||
\symAMSa=\mathgroup4
|
||||
\symAMSb=\mathgroup5
|
||||
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
|
||||
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
|
||||
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
|
||||
))
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsthm.sty
|
||||
Package: amsthm 2020/05/29 v2.20.6
|
||||
\thm@style=\toks21
|
||||
\thm@bodyfont=\toks22
|
||||
\thm@headfont=\toks23
|
||||
\thm@notefont=\toks24
|
||||
\thm@headpunct=\toks25
|
||||
\thm@preskip=\skip52
|
||||
\thm@postskip=\skip53
|
||||
\thm@headsep=\skip54
|
||||
\dth@everypar=\toks26
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
|
||||
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
|
||||
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
|
||||
\KV@toks@=\toks27
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
|
||||
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
|
||||
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
|
||||
)
|
||||
Package graphics Info: Driver file: pdftex.def on input line 107.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
|
||||
))
|
||||
\Gin@req@height=\dimen148
|
||||
\Gin@req@width=\dimen149
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/geometry/geometry.sty
|
||||
Package: geometry 2020/01/02 v5.9 Page Geometry
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/ifvtex.sty
|
||||
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/iftex.sty
|
||||
Package: iftex 2022/02/03 v1.0f TeX engine tests
|
||||
))
|
||||
\Gm@cnth=\count271
|
||||
\Gm@cntv=\count272
|
||||
\c@Gm@tempcnt=\count273
|
||||
\Gm@bindingoffset=\dimen150
|
||||
\Gm@wd@mp=\dimen151
|
||||
\Gm@odd@mp=\dimen152
|
||||
\Gm@even@mp=\dimen153
|
||||
\Gm@layoutwidth=\dimen154
|
||||
\Gm@layoutheight=\dimen155
|
||||
\Gm@layouthoffset=\dimen156
|
||||
\Gm@layoutvoffset=\dimen157
|
||||
\Gm@dimlist=\toks28
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/booktabs/booktabs.sty
|
||||
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
|
||||
\heavyrulewidth=\dimen158
|
||||
\lightrulewidth=\dimen159
|
||||
\cmidrulewidth=\dimen160
|
||||
\belowrulesep=\dimen161
|
||||
\belowbottomsep=\dimen162
|
||||
\aboverulesep=\dimen163
|
||||
\abovetopsep=\dimen164
|
||||
\cmidrulesep=\dimen165
|
||||
\cmidrulekern=\dimen166
|
||||
\defaultaddspace=\dimen167
|
||||
\@cmidla=\count274
|
||||
\@cmidlb=\count275
|
||||
\@aboverulesep=\dimen168
|
||||
\@belowrulesep=\dimen169
|
||||
\@thisruleclass=\count276
|
||||
\@lastruleclass=\count277
|
||||
\@thisrulewidth=\dimen170
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
|
||||
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
|
||||
\l__color_backend_stack_int=\count278
|
||||
\l__pdf_internal_box=\box52
|
||||
)
|
||||
No file double_contraction_reductio.aux.
|
||||
\openout1 = `double_contraction_reductio.aux'.
|
||||
|
||||
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 20.
|
||||
LaTeX Font Info: ... okay on input line 20.
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
|
||||
[Loading MPS to PDF converter (version 2006.09.02).]
|
||||
\scratchcounter=\count279
|
||||
\scratchdimen=\dimen171
|
||||
\scratchbox=\box53
|
||||
\nofMPsegments=\count280
|
||||
\nofMParguments=\count281
|
||||
\everyMPshowfont=\toks29
|
||||
\MPscratchCnt=\count282
|
||||
\MPscratchDim=\dimen172
|
||||
\MPnumerator=\count283
|
||||
\makeMPintoPDFobject=\count284
|
||||
\everyMPtoPDFconversion=\toks30
|
||||
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
|
||||
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
|
||||
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
|
||||
85.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
|
||||
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
|
||||
e
|
||||
))
|
||||
*geometry* driver: auto-detecting
|
||||
*geometry* detected driver: pdftex
|
||||
*geometry* verbose mode - [ preamble ] result:
|
||||
* driver: pdftex
|
||||
* paper: <default>
|
||||
* layout: <same size as paper>
|
||||
* layoutoffset:(h,v)=(0.0pt,0.0pt)
|
||||
* modes:
|
||||
* h-part:(L,W,R)=(72.26999pt, 469.75502pt, 72.26999pt)
|
||||
* v-part:(T,H,B)=(72.26999pt, 650.43001pt, 72.26999pt)
|
||||
* \paperwidth=614.295pt
|
||||
* \paperheight=794.96999pt
|
||||
* \textwidth=469.75502pt
|
||||
* \textheight=650.43001pt
|
||||
* \oddsidemargin=0.0pt
|
||||
* \evensidemargin=0.0pt
|
||||
* \topmargin=-37.0pt
|
||||
* \headheight=12.0pt
|
||||
* \headsep=25.0pt
|
||||
* \topskip=11.0pt
|
||||
* \footskip=30.0pt
|
||||
* \marginparwidth=59.0pt
|
||||
* \marginparsep=10.0pt
|
||||
* \columnsep=10.0pt
|
||||
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
|
||||
* \hoffset=0.0pt
|
||||
* \voffset=0.0pt
|
||||
* \mag=1000
|
||||
* \@twocolumnfalse
|
||||
* \@twosidefalse
|
||||
* \@mparswitchfalse
|
||||
* \@reversemarginfalse
|
||||
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
|
||||
|
||||
LaTeX Font Info: Trying to load font information for U+msa on input line 21.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
|
||||
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
|
||||
)
|
||||
LaTeX Font Info: Trying to load font information for U+msb on input line 21.
|
||||
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
|
||||
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
|
||||
) [1
|
||||
|
||||
{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] [2]
|
||||
(./double_contraction_reductio.aux) )
|
||||
Here is how much of TeX's memory you used:
|
||||
3256 strings out of 478268
|
||||
48463 string characters out of 5846347
|
||||
347695 words of memory out of 5000000
|
||||
21442 multiletter control sequences out of 15000+600000
|
||||
479484 words of font info for 65 fonts, out of 8000000 for 9000
|
||||
1141 hyphenation exceptions out of 8191
|
||||
55i,5n,62p,244b,218s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
{/usr/local/texlive/2022/texmf-dist/fonts/
|
||||
enc/dvips/cm-super/cm-super-ts1.enc}</usr/local/texlive/2022/texmf-dist/fonts/t
|
||||
ype1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
|
||||
pe1/public/amsfonts/cm/cmbx12.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
|
||||
e1/public/amsfonts/cm/cmbxti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
|
||||
pe1/public/amsfonts/cm/cmitt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
|
||||
pe1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
|
||||
e1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
|
||||
/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
|
||||
ublic/amsfonts/cm/cmr17.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
|
||||
lic/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
|
||||
/amsfonts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
|
||||
msfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmsy8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
|
||||
onts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
|
||||
nts/symbols/msam10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/c
|
||||
m-super/sfrm1095.pfb>
|
||||
Output written on double_contraction_reductio.pdf (2 pages, 176278 bytes).
|
||||
PDF statistics:
|
||||
87 PDF objects out of 1000 (max. 8388607)
|
||||
52 compressed objects within 1 object stream
|
||||
0 named destinations out of 1000 (max. 500000)
|
||||
1 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
\documentclass[11pt]{article}
|
||||
\usepackage{amsmath,amssymb,amsthm}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{geometry}
|
||||
\usepackage{booktabs}
|
||||
\geometry{margin=1in}
|
||||
|
||||
\title{A double-contraction reductio at a degree-5 vertex\\
|
||||
via Kempe chains as Heawood face-chains}
|
||||
\author{}
|
||||
\date{}
|
||||
|
||||
\newtheorem*{obs}{Observation}
|
||||
\newtheorem*{prop}{Proposition}
|
||||
\newtheorem*{conj}{Conjecture}
|
||||
\newtheorem*{lem}{Lemma}
|
||||
\newtheorem*{claim}{Claim}
|
||||
\newtheorem*{remk}{Remark}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\emph{Status: exploratory strategy note. Records a proof skeleton and the
|
||||
single lemma it reduces to; nothing here is proved. Companion to
|
||||
\texttt{boundary\_restriction\_structure.tex} and \texttt{paper.tex}.}
|
||||
|
||||
\section*{Setup: the move}
|
||||
|
||||
Let $G$ be a minimal counterexample to the Four Colour Theorem (a smallest
|
||||
triangulation with no proper $4$-vertex-colouring). By Euler $G$ has a
|
||||
vertex $v$ of degree $\le 5$; degrees $\le 4$ are classically reducible, so
|
||||
take $\deg(v)=5$. The link of $v$ is a pentagon $a,x,b,y,z$ (consecutive
|
||||
neighbours adjacent). For a non-adjacent (diagonal) pair, say $a,b$, the
|
||||
\emph{double contraction} contracts both edges $(a,v)$ and $(b,v)$,
|
||||
identifying $a=v=b$ into a single vertex. There are $5$ diagonal pairs.
|
||||
|
||||
The double contraction is a proper minor with two fewer vertices, hence a
|
||||
smaller planar graph $G'$; by minimality $G'$ is $4$-colourable. It stays a
|
||||
simple triangulation iff each contracted edge is non-separating (its
|
||||
endpoints have exactly two common neighbours).
|
||||
|
||||
\section*{Kempe chains as Heawood face-chains}
|
||||
|
||||
Work in the cubic dual $G'$ (vertices $=$ faces of $G$, so the Heawood
|
||||
$\pm1$ labels sit on dual vertices). Tait-colour the edges with the three
|
||||
nonzero Klein-4 elements $\{a,b,c\}$ (edge colour $=$ colour-difference
|
||||
across the primal edge). Then:
|
||||
|
||||
\begin{obs}[Kempe chain $=$ Tait cycle $=$ flip-set]
|
||||
A primal Kempe chain (component of two vertex-colour classes of $G$)
|
||||
corresponds to a connected component of the two-edge-colour subgraph
|
||||
$\{a,b\}$ of the dual. That subgraph is $2$-regular, so it is a disjoint
|
||||
union of cycles; each such cycle alternates $a,b,a,b,\dots$ and is therefore
|
||||
\textbf{always even}. The cycle is literally a cyclic chain of faces of $G$.
|
||||
A Kempe swap reverses the rotational order $(a,b,c)$ at exactly the vertices
|
||||
on the cycle, so the set of faces whose Heawood sign \emph{flips} under the
|
||||
swap is precisely the Kempe chain.
|
||||
\end{obs}
|
||||
|
||||
\begin{obs}[Sign alternation $=$ same-side rule]
|
||||
Even-ness does \emph{not} force the Heawood signs to alternate along the
|
||||
cycle. At each cycle vertex the third (off-cycle, colour-$c$) edge points
|
||||
either inside or outside the region bounded by the cycle. For consecutive
|
||||
vertices $A,B$:
|
||||
\[
|
||||
\text{same side (both in / both out)} \Rightarrow \text{signs flip;}
|
||||
\qquad
|
||||
\text{opposite side} \Rightarrow \text{signs repeat.}
|
||||
\]
|
||||
(Local chirality computation: the four in/out cases give
|
||||
$+,-$ / $-,+$ / $+,+$ / $-,-$.) Hence the Heawood sign sequence around a
|
||||
Tait cycle is determined, up to one global sign, by the in/out pattern of
|
||||
the third edges; it is fully alternating iff there are zero side-switches,
|
||||
i.e.\ all third edges lie on one side.
|
||||
\end{obs}
|
||||
|
||||
\section*{The transport hypothesis (L1)}
|
||||
|
||||
\begin{conj}[Chain transport, claimed for all triangulations]
|
||||
The double contraction preserves the relevant (crossing) Kempe/Heawood
|
||||
chain structure as it propagates up the nested tire decomposition: the two
|
||||
chains anchored at the pentagon survive, consistently, through every tire
|
||||
interface above $v$.
|
||||
\end{conj}
|
||||
|
||||
\section*{The reductio (two nested contradictions)}
|
||||
|
||||
We are \emph{not} claiming $G'$ is uncolourable, and we are not claiming
|
||||
intertwined Kempe chains are impossible (they occur, e.g.\ in the Errera
|
||||
graph). The reducible object is \emph{forced} intertwining.
|
||||
|
||||
\paragraph{Inner.} Suppose intertwining is the \emph{only} way to colour
|
||||
$G'$ (every colouring is intertwined at $v$). By transport the two crossing
|
||||
chains pass through every interface above; if uncrossing is excluded at each
|
||||
interface, the tire restriction relations $R_{\mathsf K}$ collapse to their
|
||||
``crossed-only'' sub-relations. Contract each interface \textbf{along the
|
||||
chain} to obtain a strictly smaller triangulation $H''$. If the crossed-only
|
||||
sub-relations admit no compatible gluing on $H''$, then $H''$ is
|
||||
uncolourable and smaller than $G$ --- contradicting minimality.
|
||||
|
||||
\paragraph{Outer.} Therefore $G'$ admits a non-intertwined colouring; it
|
||||
uncrosses at $v$, frees a colour for $v$, and lifts to a colouring of $G$ ---
|
||||
contradicting that $G$ is a counterexample. \hfill$\square$ (modulo the
|
||||
lemma below)
|
||||
|
||||
\section*{What it all reduces to}
|
||||
|
||||
\begin{claim}[the only open content]
|
||||
Under the crossed-only collapse, the surviving Heawood boundary sequences at
|
||||
a (forced-short) tire interface have empty pointwise-negation gluing on
|
||||
$H''$ --- not merely small, genuinely empty.
|
||||
\end{claim}
|
||||
|
||||
This is where the $2^{n-2}$ constraint floor and the mod-3 side-pattern must
|
||||
do real work: the floor guarantees that \emph{smallness alone} never empties
|
||||
a relation, so the emptiness must come from the chains pinning the
|
||||
sub-relation down, not from short interface length.
|
||||
|
||||
\section*{Open points / to pin next}
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Define $H''$ precisely.} Current candidate: contract each tire
|
||||
interface along the transported chain (the global analogue of the local
|
||||
double contraction). Confirm this is a definite, strictly smaller
|
||||
triangulation.
|
||||
\item \textbf{Lift is not automatic.} ``Non-intertwined'' must be defined as
|
||||
``admits a swap collapsing the pentagon $a,x,b,y,z$ to $\le 3$ colours,''
|
||||
or freeing $v$ fails even after uncrossing (with $c(a)=c(b)$ the pentagon
|
||||
can still show $4$ colours).
|
||||
\item \textbf{Errera oracle.} Run the whole construction against the Errera
|
||||
graph (and Fritsch / Kittell). These are colourable but Kempe-intertwined;
|
||||
the hypothesis ``every colouring intertwined'' must \emph{fail} for them,
|
||||
so the construction must decline. \emph{Why} it declines is exactly the
|
||||
mechanism the Claim must deny in the counterexample case.
|
||||
\item \textbf{Prove or break L1} as a statement about all triangulations.
|
||||
\end{itemize}
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,56 @@
|
||||
\relax
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{Heawood1898}
|
||||
\citation{bauerfeld-medial-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Connected tire clusters}}{2}{}\protected@file@percent }
|
||||
\newlabel{sec:tire-clusters}{{2}{2}}
|
||||
\newlabel{lem:same-depth-vertex-meet}{{2.1}{2}}
|
||||
\newlabel{def:connected-tire-cluster}{{2.2}{2}}
|
||||
\newlabel{rem:cluster-cut-vertices}{{2.3}{2}}
|
||||
\newlabel{prop:two-clusters-per-vertex}{{2.4}{2}}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Heawood restrictions on the tire dual}}{3}{}\protected@file@percent }
|
||||
\newlabel{sec:heawood-restrictions}{{3}{3}}
|
||||
\newlabel{def:heawood-labelling}{{3.1}{3}}
|
||||
\newlabel{rem:no-interior-constraint}{{3.2}{3}}
|
||||
\newlabel{def:boundary-sequences}{{3.3}{3}}
|
||||
\newlabel{def:heawood-compatible}{{3.4}{3}}
|
||||
\citation{Heawood1898}
|
||||
\newlabel{rem:compat-is-heawood}{{3.5}{4}}
|
||||
\newlabel{eq:heawood-face-sum-dual}{{3.1}{4}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Why the programme runs between nested clusters}}{4}{}\protected@file@percent }
|
||||
\newlabel{prop:two-sided-decomposition}{{3.6}{4}}
|
||||
\citation{bauerfeld-nested-tires}
|
||||
\newlabel{rem:why-clusters}{{3.7}{5}}
|
||||
\newlabel{conj:heawood-chain-pigeonhole}{{3.8}{5}}
|
||||
\newlabel{conj:heawood-route-fct}{{3.9}{5}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{The constraint floor}}{6}{}\protected@file@percent }
|
||||
\newlabel{sec:constraint-floor}{{4}{6}}
|
||||
\newlabel{def:achievable-boundary-set}{{4.1}{6}}
|
||||
\newlabel{prop:attainment}{{4.2}{6}}
|
||||
\newlabel{lem:unstack}{{4.3}{6}}
|
||||
\newlabel{conj:constraint-floor}{{4.4}{6}}
|
||||
\newlabel{rem:floor-status}{{4.5}{6}}
|
||||
\bibcite{Heawood1898}{1}
|
||||
\bibcite{bauerfeld-depth}{2}
|
||||
\bibcite{bauerfeld-nested-tires}{3}
|
||||
\bibcite{bauerfeld-medial-tires}{4}
|
||||
\bibcite{bauerfeld-nested-tire-duals}{5}
|
||||
\newlabel{tocindent-1}{0pt}
|
||||
\newlabel{tocindent0}{14.69437pt}
|
||||
\newlabel{tocindent1}{17.77782pt}
|
||||
\newlabel{tocindent2}{0pt}
|
||||
\newlabel{tocindent3}{0pt}
|
||||
\newlabel{rem:floor-consequences}{{4.6}{7}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{7}{}\protected@file@percent }
|
||||
\gdef \@abspage@last{7}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Fdb version 3
|
||||
["pdflatex"] 1781668577 "paper.tex" "paper.pdf" "paper" 1781668578
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam10.tfm" 1246382020 916 f87d7c45f9c908e672703b83b72241a3 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam5.tfm" 1246382020 924 9904cf1d39e9767e7a3622f2a125a565 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam7.tfm" 1246382020 928 2dc8d444221b7a635bb58038579b861a ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm10.tfm" 1246382020 908 2921f8a10601f252058503cc6570e581 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm5.tfm" 1246382020 940 75ac932a52f80982a9f8ea75d03a34cf ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm7.tfm" 1246382020 940 228d6584342e91276bf566bcf9716b83 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm" 1136768653 1328 c834bbb027764024c09d3d2bf908b5f0 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm" 1136768653 1332 1fde11373e221473104d6cc5993f046e ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmcsc10.tfm" 1136768653 1300 63a6111ee6274895728663cf4b4e7e81 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1136768653 1512 f21f83efb36853c0b70002322c1ab3ad ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1136768653 1520 eccf95517727cb11801f4f1aee3a21b4 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr6.tfm" 1136768653 1300 b62933e007d01cfd073f79b963c01526 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr8.tfm" 1136768653 1292 21c1c5bfeaebccffdb478fd231a0997d ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb" 1248133631 32166 b0c356b15f19587482a9217ce1d8fa67 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1461363279 71627 94eb9990bed73c364d7f53f960cc8c5b ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls" 1591045760 61881 a7369c346c2922a758ae6283cc1ed014 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty" 1359763108 5949 3f3fd50a8cc94c3d4cbf4fc66cd3df1c ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty" 1359763108 13829 94730e64147574077f8ecfea9bb69af4 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd" 1359763108 961 6518c6525a34feb5e8250ffa91731cff ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd" 1359763108 961 d02606146ba5601b5645f987c92e6193 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty" 1622667781 2222 da905dc1db75412efd2d8f67739f0596 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty" 1622667781 4173 bc0410bcccdff806d6132d3c1ef35481 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty" 1636758526 87648 07fbb6e9169e00cb2a2f40b31b2dbf3c ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty" 1636758526 4128 8eea906621b6639f7ba476a472036bbe ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty" 1636758526 2444 926f379cc60fcf0c6e3fee2223b4370d ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty" 1579991033 13886 d1306dcf79a944f6988e688c1785f9ce ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1465944070 1224 978390e9c2234eab29404bc21b268d1e ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def" 1601931164 19103 48d29b6e2a64cb717117ef65f107b404 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty" 1622581934 18399 7e40f80366dffb22c0e7b70517db5cb4 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty" 1636758526 7996 a8fb260d598dcaf305a7ae7b9c3e3229 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty" 1622581934 2671 4de6781a30211fe0ea4c672e4a2a8166 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty" 1636758526 4009 187ea2dc3194cd5a76cd99a8d7a6c4d0 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def" 1644269979 29921 d0acc05a38bd4aa3af2017f0b7c137ce ""
|
||||
"/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg" 1279039959 678 4792914a8f45be57bb98413425e4c7af ""
|
||||
"/usr/local/texlive/2022/texmf-dist/web2c/texmf.cnf" 1646502317 40171 cdab547de63d26590bebb3baff566530 ""
|
||||
"/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 ""
|
||||
"/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
|
||||
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
|
||||
"paper.aux" 1781668577 894 c07070c22299dc3a5b11f2d70e9e6864 "pdflatex"
|
||||
"paper.tex" 1781668572 3984 c8e5ad80c1dfc803df154b6857fc59b0 ""
|
||||
(generated)
|
||||
"paper.aux"
|
||||
"paper.log"
|
||||
"paper.pdf"
|
||||
@@ -0,0 +1,231 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 17 JUN 2026 21:31
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
**paper.tex
|
||||
(./paper.tex
|
||||
LaTeX2e <2021-11-15> patch level 1
|
||||
L3 programming layer <2022-02-24>
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
|
||||
Document Class: amsart 2020/05/29 v2.20.6
|
||||
\linespacing=\dimen138
|
||||
\normalparindent=\dimen139
|
||||
\normaltopskip=\skip47
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
|
||||
Package: amsmath 2021/10/15 v2.17l AMS math features
|
||||
\@mathmargin=\skip48
|
||||
|
||||
For additional information on amsmath, use the `?' option.
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
|
||||
Package: amstext 2021/08/26 v2.01 AMS text
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
|
||||
File: amsgen.sty 1999/11/30 v2.0 generic functions
|
||||
\@emptytoks=\toks16
|
||||
\ex@=\dimen140
|
||||
))
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
|
||||
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
|
||||
\pmbraise@=\dimen141
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
|
||||
Package: amsopn 2021/08/26 v2.02 operator names
|
||||
)
|
||||
\inf@bad=\count185
|
||||
LaTeX Info: Redefining \frac on input line 234.
|
||||
\uproot@=\count186
|
||||
\leftroot@=\count187
|
||||
LaTeX Info: Redefining \overline on input line 399.
|
||||
\classnum@=\count188
|
||||
\DOTSCASE@=\count189
|
||||
LaTeX Info: Redefining \ldots on input line 496.
|
||||
LaTeX Info: Redefining \dots on input line 499.
|
||||
LaTeX Info: Redefining \cdots on input line 620.
|
||||
\Mathstrutbox@=\box50
|
||||
\strutbox@=\box51
|
||||
\big@size=\dimen142
|
||||
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
|
||||
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
|
||||
\macc@depth=\count190
|
||||
\c@MaxMatrixCols=\count191
|
||||
\dotsspace@=\muskip16
|
||||
\c@parentequation=\count192
|
||||
\dspbrk@lvl=\count193
|
||||
\tag@help=\toks17
|
||||
\row@=\count194
|
||||
\column@=\count195
|
||||
\maxfields@=\count196
|
||||
\andhelp@=\toks18
|
||||
\eqnshift@=\dimen143
|
||||
\alignsep@=\dimen144
|
||||
\tagshift@=\dimen145
|
||||
\tagwidth@=\dimen146
|
||||
\totwidth@=\dimen147
|
||||
\lineht@=\dimen148
|
||||
\@envbody=\toks19
|
||||
\multlinegap=\skip49
|
||||
\multlinetaggap=\skip50
|
||||
\mathdisplay@stack=\toks20
|
||||
LaTeX Info: Redefining \[ on input line 2938.
|
||||
LaTeX Info: Redefining \] on input line 2939.
|
||||
)
|
||||
LaTeX Font Info: Trying to load font information for U+msa on input line 397
|
||||
.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
|
||||
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
|
||||
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
|
||||
\symAMSa=\mathgroup4
|
||||
\symAMSb=\mathgroup5
|
||||
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
|
||||
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
|
||||
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
|
||||
)
|
||||
\copyins=\insert199
|
||||
\abstractbox=\box52
|
||||
\listisep=\skip51
|
||||
\c@part=\count197
|
||||
\c@section=\count198
|
||||
\c@subsection=\count266
|
||||
\c@subsubsection=\count267
|
||||
\c@paragraph=\count268
|
||||
\c@subparagraph=\count269
|
||||
\c@figure=\count270
|
||||
\c@table=\count271
|
||||
\abovecaptionskip=\skip52
|
||||
\belowcaptionskip=\skip53
|
||||
\captionindent=\dimen149
|
||||
\thm@style=\toks21
|
||||
\thm@bodyfont=\toks22
|
||||
\thm@headfont=\toks23
|
||||
\thm@notefont=\toks24
|
||||
\thm@headpunct=\toks25
|
||||
\thm@preskip=\skip54
|
||||
\thm@postskip=\skip55
|
||||
\thm@headsep=\skip56
|
||||
\dth@everypar=\toks26
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
|
||||
Package: amssymb 2013/01/14 v3.01 AMS font symbols
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
|
||||
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
|
||||
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
|
||||
\KV@toks@=\toks27
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
|
||||
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
|
||||
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
|
||||
)
|
||||
Package graphics Info: Driver file: pdftex.def on input line 107.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
|
||||
))
|
||||
\Gin@req@height=\dimen150
|
||||
\Gin@req@width=\dimen151
|
||||
)
|
||||
\c@theorem=\count272
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
|
||||
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
|
||||
\l__color_backend_stack_int=\count273
|
||||
\l__pdf_internal_box=\box53
|
||||
)
|
||||
(./paper.aux)
|
||||
\openout1 = `paper.aux'.
|
||||
|
||||
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 27.
|
||||
LaTeX Font Info: ... okay on input line 27.
|
||||
LaTeX Font Info: Trying to load font information for U+msa on input line 27.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
|
||||
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
|
||||
)
|
||||
LaTeX Font Info: Trying to load font information for U+msb on input line 27.
|
||||
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
|
||||
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
|
||||
)
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
|
||||
[Loading MPS to PDF converter (version 2006.09.02).]
|
||||
\scratchcounter=\count274
|
||||
\scratchdimen=\dimen152
|
||||
\scratchbox=\box54
|
||||
\nofMPsegments=\count275
|
||||
\nofMParguments=\count276
|
||||
\everyMPshowfont=\toks28
|
||||
\MPscratchCnt=\count277
|
||||
\MPscratchDim=\dimen153
|
||||
\MPnumerator=\count278
|
||||
\makeMPintoPDFobject=\count279
|
||||
\everyMPtoPDFconversion=\toks29
|
||||
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
|
||||
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
|
||||
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
|
||||
85.
|
||||
|
||||
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
|
||||
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
|
||||
e
|
||||
))
|
||||
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
|
||||
[2] [3] [4] [5] [6] [7] (./paper.aux) )
|
||||
Here is how much of TeX's memory you used:
|
||||
3024 strings out of 478268
|
||||
42307 string characters out of 5846347
|
||||
342360 words of memory out of 5000000
|
||||
21070 multiletter control sequences out of 15000+600000
|
||||
477578 words of font info for 59 fonts, out of 8000000 for 9000
|
||||
1302 hyphenation exceptions out of 8191
|
||||
69i,7n,76p,242b,290s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf-dist/font
|
||||
s/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts
|
||||
/type1/public/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
|
||||
ype1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
|
||||
ype1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
|
||||
pe1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
|
||||
e1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
|
||||
/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
|
||||
ublic/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
|
||||
lic/amsfonts/cm/cmr5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
|
||||
/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
|
||||
nts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
|
||||
ts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
|
||||
/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
|
||||
cm/cmsy5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm
|
||||
/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/c
|
||||
mti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
|
||||
ti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols
|
||||
/msam10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/sym
|
||||
bols/msbm10.pfb>
|
||||
Output written on paper.pdf (7 pages, 269140 bytes).
|
||||
PDF statistics:
|
||||
128 PDF objects out of 1000 (max. 8388607)
|
||||
78 compressed objects within 1 object stream
|
||||
0 named destinations out of 1000 (max. 500000)
|
||||
1 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
%% filename: amsart-template.tex
|
||||
%% American Mathematical Society
|
||||
%% AMS-LaTeX v.2 template for use with amsart
|
||||
%% ====================================================================
|
||||
|
||||
\documentclass{amsart}
|
||||
|
||||
\usepackage{amssymb}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\newtheorem{theorem}{Theorem}[section]
|
||||
\newtheorem{lemma}[theorem]{Lemma}
|
||||
\newtheorem{corollary}[theorem]{Corollary}
|
||||
\newtheorem{proposition}[theorem]{Proposition}
|
||||
\newtheorem{conjecture}[theorem]{Conjecture}
|
||||
|
||||
\theoremstyle{definition}
|
||||
\newtheorem{definition}[theorem]{Definition}
|
||||
\newtheorem{example}[theorem]{Example}
|
||||
\newtheorem{xca}[theorem]{Exercise}
|
||||
|
||||
\theoremstyle{remark}
|
||||
\newtheorem{remark}[theorem]{Remark}
|
||||
|
||||
\numberwithin{equation}{section}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\title{Heawood Restrictions on Nested Tire Graph Duals}
|
||||
|
||||
% author one information
|
||||
\author{Eric Bauerfeld}
|
||||
\address{}
|
||||
\curraddr{}
|
||||
\email{}
|
||||
\thanks{}
|
||||
|
||||
\subjclass[2010]{Primary }
|
||||
|
||||
\keywords{plane graph, triangulation, plane depth, level edge, dual graph,
|
||||
tire graph, Heawood number}
|
||||
|
||||
\date{}
|
||||
|
||||
\dedicatory{}
|
||||
|
||||
\begin{abstract}
|
||||
%% TODO: abstract. Following \cite{bauerfeld-nested-tires}, which establishes
|
||||
%% the basic vocabulary of tire graphs and dual depth, we study the Heawood
|
||||
%% (mod-3 / face-sum) restrictions imposed on the duals of nested tire graphs.
|
||||
\end{abstract}
|
||||
|
||||
\maketitle
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
A classical theorem of Tait recasts the Four Colour Theorem in dual,
|
||||
edge-colouring terms: a plane triangulation $G$ is properly $4$-vertex-colourable
|
||||
if and only if its dual cubic graph $G'$ is properly $3$-edge-colourable. Thus a
|
||||
minimal counterexample to the Four Colour Theorem -- a smallest triangulation
|
||||
admitting no proper $4$-colouring -- corresponds to a smallest cubic plane graph
|
||||
admitting no proper $3$-edge-colouring.
|
||||
|
||||
This paper continues the series studying that structure through the
|
||||
lens of \emph{nested level duals}. The foundational vocabulary ---
|
||||
level sources, levels, the inner planar dual $G'$ and its dual depth,
|
||||
and tire graphs --- is developed in the companion paper
|
||||
\cite{bauerfeld-nested-tires}; we refer to that paper for those
|
||||
definitions and rely on them throughout. In particular we use,
|
||||
without restating, the notions of:
|
||||
\begin{itemize}
|
||||
\item \emph{level source} $S$ and $G$-vertex levels $\ell_G(v)$;
|
||||
\item the inner planar dual $G'$
|
||||
(\cite[Definition~1.3]{bauerfeld-nested-tires});
|
||||
\item \emph{dual depth} $\delta_G(d_f)$
|
||||
(\cite[Definition~1.4]{bauerfeld-nested-tires});
|
||||
\item \emph{tire graph} $T = (B_{\mathrm{out}}, O, E_{\mathrm{ann}})$
|
||||
with outer/inner boundaries and annular edges
|
||||
(\cite[Definition~1.5]{bauerfeld-nested-tires});
|
||||
\item the \emph{tire-component lemma}
|
||||
(\cite[Lemma~1.8]{bauerfeld-nested-tires}); and
|
||||
\item the \emph{tire-tread partition theorem}
|
||||
(\cite[Theorem~1.9]{bauerfeld-nested-tires}).
|
||||
\end{itemize}
|
||||
|
||||
Throughout, $G = (V, E)$ is a plane maximal planar graph (a triangulation)
|
||||
with a fixed planar embedding $\Pi_G$. We write $|V| = n$, so $|E| = 3n - 6$
|
||||
and $G$ has $2n - 4$ triangular faces.
|
||||
|
||||
The classical input is Heawood's face-sum identity \cite{Heawood1898}:
|
||||
for any proper $3$-edge-colouring of a cubic plane graph $H$, assigning
|
||||
each face of $H$ a number in $\{+1, -1\}$ can be done so that the labels
|
||||
around every vertex of $H$ sum to $0 \pmod 3$. In the triangulation
|
||||
$G$ dual to $H$ this becomes a $\{+1, -1\}$ labelling of the
|
||||
\emph{faces} of $G$ whose incident-face sum at every vertex of $G$
|
||||
vanishes mod $3$. Our aim is to record what this restriction forces
|
||||
along the boundary cycles of a nested tire graph, and to formulate a
|
||||
chain-pigeonhole programme in this Heawood labelling parallel to the
|
||||
medial programme of \cite{bauerfeld-medial-tires}.
|
||||
|
||||
\section{Connected tire clusters}
|
||||
\label{sec:tire-clusters}
|
||||
|
||||
The tire treads at a fixed depth partition the depth-$d$ faces of $G$
|
||||
\cite{bauerfeld-nested-tires}, but distinct depth-$d$ tires need not be
|
||||
vertex-disjoint: a single vertex of $G$ may lie on the source-side
|
||||
boundary of several depth-$d$ tires at once (this occurs exactly when
|
||||
the depth-$d$ faces around that vertex are split into more than one arc
|
||||
by depth-$(d{-}1)$ faces). We organise the depth-$d$ tires by this
|
||||
sharing.
|
||||
|
||||
\begin{lemma}[Same-depth tires meet only in vertices]
|
||||
\label{lem:same-depth-vertex-meet}
|
||||
Let $T \neq T'$ be two distinct tire treads at the same depth $d$ in
|
||||
$\mathcal{T}(G, S)$, arising from connected components $C', C''$ of the
|
||||
depth-$d$ dual subgraph $G'_d$. Then $T$ and $T'$ share no edge of $G$;
|
||||
any intersection $V(T) \cap V(T')$ consists of isolated vertices.
|
||||
\end{lemma}
|
||||
|
||||
\begin{proof}
|
||||
An edge $e$ of $G$ shared by two depth-$d$ annular faces $f_1, f_2$ is,
|
||||
by definition of the inner dual \cite[Definition~1.3]{bauerfeld-nested-tires},
|
||||
a dual edge of $G'$ joining $d_{f_1}$ and $d_{f_2}$; since $\delta(d_{f_1})
|
||||
= \delta(d_{f_2}) = d$, this edge lies in $G'_d$, so $d_{f_1}$ and
|
||||
$d_{f_2}$ belong to the same component of $G'_d$. Hence no edge of $G$
|
||||
is shared by annular faces of two \emph{different} components, and
|
||||
distinct depth-$d$ tires share no edge. Their intersection is therefore
|
||||
a set of isolated vertices.
|
||||
\end{proof}
|
||||
|
||||
\begin{definition}[Connected tire cluster]
|
||||
\label{def:connected-tire-cluster}
|
||||
Fix a nested tire decomposition $\mathcal{T}(G, S)$ and a depth $d$. On
|
||||
the set of depth-$d$ tire treads define the relation
|
||||
\[
|
||||
T \sim T' \quad\Longleftrightarrow\quad V(T) \cap V(T') \neq \varnothing .
|
||||
\]
|
||||
A \emph{connected tire cluster} at depth $d$ is the subgraph of $G$
|
||||
\[
|
||||
\mathsf{K} \;=\; \bigcup_{i} T_i \;\subseteq\; G
|
||||
\]
|
||||
obtained as the union (of underlying plane graphs) of the tires in a
|
||||
single connected component $\{T_i\}$ of the transitive closure of
|
||||
$\sim$. A cluster consisting of a single tire is \emph{trivial}; the
|
||||
connected tire clusters at depth $d$ partition the depth-$d$ tires.
|
||||
\end{definition}
|
||||
|
||||
\begin{remark}
|
||||
\label{rem:cluster-cut-vertices}
|
||||
By Lemma~\ref{lem:same-depth-vertex-meet} the constituent tires of a
|
||||
connected tire cluster are joined only at shared vertices, each of which
|
||||
is a cut vertex of $\mathsf{K}$; a connected tire cluster is thus a
|
||||
``cactus of tires'' and is in general \emph{not} itself a tire graph,
|
||||
since the annulus structure of \cite[Definition~1.5]{bauerfeld-nested-tires}
|
||||
fails at each such pinch. The shared (cut) vertices are precisely the
|
||||
vertices that belong to more than one depth-$d$ tire.
|
||||
\end{remark}
|
||||
|
||||
A single vertex may belong to several tires at one depth --- the
|
||||
high-degree case where its depth-$d$ faces split into many arcs --- so
|
||||
the number of \emph{tires} through a vertex is unbounded. Clustering
|
||||
collapses exactly this multiplicity: all tires through a vertex at a
|
||||
fixed depth share that vertex, hence lie in one cluster. The cluster
|
||||
count is therefore controlled.
|
||||
|
||||
\begin{proposition}[A vertex meets at most two clusters]
|
||||
\label{prop:two-clusters-per-vertex}
|
||||
Every vertex $v \in V(G)$ belongs to at most two connected tire
|
||||
clusters, namely at most one at each of the two consecutive depths
|
||||
$\ell_G(v) - 1$ and $\ell_G(v)$. In particular a source vertex
|
||||
($\ell_G(v) = 0$) belongs to a single cluster.
|
||||
\end{proposition}
|
||||
|
||||
\begin{proof}
|
||||
Write $\ell = \ell_G(v)$.
|
||||
|
||||
\emph{Step 1: every bounded face incident to $v$ has dual depth
|
||||
$\ell - 1$ or $\ell$.} Let $f$ be a bounded triangular face with
|
||||
$v \in V(f)$. Then
|
||||
$\delta_G(d_f) = \min_{u \in V(f)} \ell_G(u) \le \ell_G(v) = \ell$.
|
||||
The other two vertices of $f$ are adjacent to $v$ in $G$, and the level
|
||||
function $\ell_G(\cdot) = \mathrm{dist}_G(\cdot, S)$ is $1$-Lipschitz
|
||||
along edges, so each has level at least $\ell - 1$; hence
|
||||
$\delta_G(d_f) \ge \ell - 1$. Thus $\delta_G(d_f) \in \{\ell-1, \ell\}$
|
||||
(only $\delta_G(d_f) = 0$ when $\ell = 0$), so $v$ bounds faces of, and
|
||||
therefore belongs to tires of, no depth other than $\ell - 1$ or
|
||||
$\ell$.
|
||||
|
||||
\emph{Step 2: at each depth, all tires through $v$ lie in one cluster.}
|
||||
Fix $d \in \{\ell-1, \ell\}$ and let $T, T'$ be depth-$d$ tires with
|
||||
$v \in V(T) \cap V(T')$. Then $V(T) \cap V(T') \ne \varnothing$, so
|
||||
$T \sim T'$ in the sense of
|
||||
Definition~\ref{def:connected-tire-cluster}, and all depth-$d$ tires
|
||||
containing $v$ lie in a single connected component of $\sim$ --- one
|
||||
connected tire cluster $\mathsf{K}_d$.
|
||||
|
||||
Combining the two steps, $v$ belongs to at most the clusters
|
||||
$\mathsf{K}_{\ell-1}$ and $\mathsf{K}_{\ell}$, i.e.\ to at most two
|
||||
connected tire clusters; when $\ell = 0$ only $\mathsf{K}_0$ occurs.
|
||||
\end{proof}
|
||||
|
||||
\section{Heawood restrictions on the tire dual}
|
||||
\label{sec:heawood-restrictions}
|
||||
|
||||
We work inside a fixed nested tire decomposition $\mathcal{T}(G, S)$ of
|
||||
$G$ from a single-vertex level source $S$ \cite{bauerfeld-nested-tires},
|
||||
and use the tire data $T = (B_{\mathrm{out}}, O, E_{\mathrm{ann}})$ with
|
||||
annular faces $F_{\mathrm{ann}}$, outer boundary $B_{\mathrm{out}}$, and
|
||||
inner boundary $B_{\mathrm{in}}$
|
||||
(\cite[Definition~1.5]{bauerfeld-nested-tires}). Since $O$ is
|
||||
outerplanar, every vertex of a tire lies on $B_{\mathrm{out}}$ or on the
|
||||
inner-boundary walk $B_{\mathrm{in}}$; a tire has no interior vertices.
|
||||
|
||||
\begin{definition}[Heawood face-labelling of a tire]
|
||||
\label{def:heawood-labelling}
|
||||
A \emph{Heawood face-labelling} of a tire graph $T$ is a map
|
||||
\[
|
||||
\lambda : F_{\mathrm{ann}} \longrightarrow \{+1, -1\}
|
||||
\]
|
||||
assigning a sign to each annular face of $T$. For a vertex
|
||||
$v \in V(T)$, write $F_{\mathrm{ann}}(v) \subseteq F_{\mathrm{ann}}$ for
|
||||
the set of annular faces of $T$ incident to $v$, and define the
|
||||
\emph{induced vertex value}
|
||||
\[
|
||||
\lambda^{\!*}(v) \;:=\; \sum_{f \in F_{\mathrm{ann}}(v)} \lambda(f)
|
||||
\;\;\bmod 3 \;\in\; \{0, 1, -1\}.
|
||||
\]
|
||||
The value $\lambda^{\!*}(v)$ is the \emph{partial} face-sum at $v$ taken
|
||||
over the annular faces of $T$ alone, not over all faces of $G$ incident
|
||||
to $v$.
|
||||
\end{definition}
|
||||
|
||||
\begin{remark}
|
||||
\label{rem:no-interior-constraint}
|
||||
Because a tire has no interior vertices, every annular face of $T$ is
|
||||
incident to $B_{\mathrm{out}} \cup B_{\mathrm{in}}$, and a Heawood
|
||||
face-labelling is subject to \emph{no} internal constraint: all
|
||||
$2^{|F_{\mathrm{ann}}|}$ sign assignments are admissible. The Heawood
|
||||
restriction is felt only on the two boundary cycles, through the induced
|
||||
vertex values $\lambda^{\!*}$.
|
||||
\end{remark}
|
||||
|
||||
\begin{definition}[Induced boundary sequences]
|
||||
\label{def:boundary-sequences}
|
||||
Let $\lambda$ be a Heawood face-labelling of $T$. Reading the vertices
|
||||
of $B_{\mathrm{out}}$ in clockwise order $v_0, v_1, \dots, v_{p-1}$, the
|
||||
\emph{outer Heawood sequence} of $(T, \lambda)$ is
|
||||
\[
|
||||
\sigma_{\mathrm{out}}(T, \lambda)
|
||||
\;:=\; \bigl(\lambda^{\!*}(v_0), \dots, \lambda^{\!*}(v_{p-1})\bigr)
|
||||
\;\in\; \{0, 1, -1\}^{p}.
|
||||
\]
|
||||
Reading the inner-boundary walk $B_{\mathrm{in}}$ in clockwise order
|
||||
$w_0, \dots, w_{q-1}$ gives the \emph{inner Heawood sequence}
|
||||
$\sigma_{\mathrm{in}}(T, \lambda) \in \{0, 1, -1\}^{q}$. The
|
||||
\emph{Heawood restriction relation} of $T$ is the set
|
||||
\[
|
||||
R_T \;:=\; \bigl\{\,
|
||||
\bigl(\sigma_{\mathrm{out}}(T, \lambda),\,
|
||||
\sigma_{\mathrm{in}}(T, \lambda)\bigr)
|
||||
\;:\; \lambda : F_{\mathrm{ann}} \to \{+1, -1\}
|
||||
\,\bigr\}
|
||||
\]
|
||||
of all (outer, inner) sequence pairs realisable by a single
|
||||
face-labelling, read up to rotation and the global sign-flip
|
||||
$\lambda \mapsto -\lambda$ (equivalently
|
||||
$\sigma \mapsto -\sigma$).
|
||||
\end{definition}
|
||||
|
||||
\begin{definition}[Heawood compatibility across an interface]
|
||||
\label{def:heawood-compatible}
|
||||
Let $T$ be a tire and $T' \in \mathcal{T}(G, S)$ a child of $T$, so the
|
||||
outer boundary cycle $B_{\mathrm{out}}^{(T')}$ coincides with a bounded
|
||||
face of $O^{(T)}$; let $\gamma$ be this shared cycle, of length $L$, and
|
||||
let $v$ range over its vertices. Heawood face-labellings $\lambda$ of
|
||||
$T$ and $\lambda'$ of $T'$ are \emph{compatible along $\gamma$} if at
|
||||
every shared vertex $v$,
|
||||
\[
|
||||
\lambda^{\!*}(v) + (\lambda')^{\!*}(v) \;\equiv\; 0 \pmod 3,
|
||||
\]
|
||||
i.e.\ $0$ is paired with $0$ and $+1$ with $-1$. Equivalently, the
|
||||
inner Heawood sequence of $T$ on $\gamma$ is the pointwise negation
|
||||
mod $3$ of the outer Heawood sequence of $T'$ on $\gamma$, after
|
||||
reversing one of the two clockwise readings to account for the opposite
|
||||
rotational senses in which $T$ and $T'$ traverse $\gamma$.
|
||||
\end{definition}
|
||||
|
||||
\begin{remark}
|
||||
\label{rem:compat-is-heawood}
|
||||
Call $v$ \emph{interior} if it is not incident to the outer face of
|
||||
$\Pi_G$. For an interior vertex every incident face is bounded, and
|
||||
compatibility along $\gamma$ at $v$ is exactly the statement that the
|
||||
incident-face sum at $v$ --- over the parent's annular faces together
|
||||
with the child's --- vanishes mod $3$:
|
||||
\begin{equation}
|
||||
\label{eq:heawood-face-sum-dual}
|
||||
\sum_{f \ni v} \lambda(f) \;\equiv\; 0 \pmod 3
|
||||
\qquad\text{for every interior vertex } v \in V(G),
|
||||
\end{equation}
|
||||
the sum ranging over the bounded faces incident to $v$. The interfaces
|
||||
of $\mathcal{T}(G, S)$ are interior level cycles, so cluster
|
||||
compatibility only ever constrains interior vertices and is untouched by
|
||||
the outer face.
|
||||
|
||||
To pass from \eqref{eq:heawood-face-sum-dual} to a colouring one must
|
||||
account for the outer face: an outer-boundary vertex is incident to the
|
||||
unbounded face $f_\infty$, whose label is omitted from the bounded sum.
|
||||
Extend $\lambda$ by a single label $\lambda(f_\infty) \in \{+1, -1\}$ on
|
||||
$f_\infty$. Then a family of Heawood face-labellings that is pairwise
|
||||
compatible along every interface of $\mathcal{T}(G, S)$ assembles into a
|
||||
$\{+1,-1\}$ labelling of \emph{all} faces of $G$ for which
|
||||
$\sum_{f \ni v} \lambda(f) \equiv 0 \pmod 3$ holds at every vertex ---
|
||||
the outer-boundary vertices now carrying $\lambda(f_\infty)$ in their
|
||||
sum. This is Heawood's face-sum identity \cite{Heawood1898} for a
|
||||
proper $3$-edge-colouring of the full cubic dual of $G$, hence (by Tait)
|
||||
a proper $4$-vertex-colouring of $G$.
|
||||
\end{remark}
|
||||
|
||||
\subsection*{Why the programme runs between nested clusters}
|
||||
|
||||
The vanishing condition \eqref{eq:heawood-face-sum-dual} at a vertex $v$
|
||||
is a constraint on the \emph{full} face-star of $v$. To run a
|
||||
pigeonhole between two objects --- a child and a parent --- we need that
|
||||
full sum to split as exactly two one-sided contributions, so that each
|
||||
vertex label is the combination of a single child value and a single
|
||||
parent value. This is true at the level of connected tire clusters, and
|
||||
\emph{false} at the level of individual tires. Extend a Heawood
|
||||
face-labelling to a connected tire cluster $\mathsf{K}$ by labelling
|
||||
every annular face of every tire of $\mathsf{K}$, and for $v \in
|
||||
V(\mathsf{K})$ write
|
||||
\[
|
||||
\lambda^{\!*}_{\mathsf{K}}(v) \;:=\;
|
||||
\sum_{f} \lambda(f) \;\bmod 3,
|
||||
\]
|
||||
the sum over the annular faces of $\mathsf{K}$ incident to $v$.
|
||||
|
||||
\begin{proposition}[Two-sided cluster decomposition at a vertex]
|
||||
\label{prop:two-sided-decomposition}
|
||||
Let $v \in V(G)$ have level $\ell = \ell_G(v)$, and let
|
||||
$\mathsf{K}_{\ell}$ and $\mathsf{K}_{\ell-1}$ be the at most two
|
||||
connected tire clusters containing $v$, of depths $\ell$ and $\ell-1$
|
||||
respectively (Proposition~\ref{prop:two-clusters-per-vertex}). Then the
|
||||
bounded faces of $G$ incident to $v$ partition into the annular faces of
|
||||
$\mathsf{K}_{\ell}$ at $v$ and the annular faces of $\mathsf{K}_{\ell-1}$
|
||||
at $v$, and
|
||||
\[
|
||||
\sum_{f \ni v} \lambda(f)
|
||||
\;\equiv\;
|
||||
\lambda^{\!*}_{\mathsf{K}_{\ell}}(v) +
|
||||
\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v)
|
||||
\pmod 3 .
|
||||
\]
|
||||
Each one-sided value $\lambda^{\!*}_{\mathsf{K}_d}(v)$ is the
|
||||
\emph{complete} sum over all depth-$d$ faces at $v$, so the Heawood
|
||||
condition \eqref{eq:heawood-face-sum-dual} at $v$ reads
|
||||
\[
|
||||
\lambda^{\!*}_{\mathsf{K}_{\ell}}(v) +
|
||||
\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v) \;\equiv\; 0 \pmod 3 ,
|
||||
\]
|
||||
a pairing between the single child cluster $\mathsf{K}_{\ell}$ and the
|
||||
single parent cluster $\mathsf{K}_{\ell-1}$. (When $\ell = 0$, or when
|
||||
$v$ bounds no depth-$\ell$ face, only one term is present.)
|
||||
\end{proposition}
|
||||
|
||||
\begin{proof}
|
||||
By Proposition~\ref{prop:two-clusters-per-vertex} (Step~1) every bounded
|
||||
face incident to $v$ has depth $\ell-1$ or $\ell$, partitioning the
|
||||
incident faces by depth; by Step~2 all depth-$\ell$ faces at $v$ lie in
|
||||
the single cluster $\mathsf{K}_{\ell}$ and all depth-$(\ell-1)$ faces at
|
||||
$v$ in $\mathsf{K}_{\ell-1}$. Hence the depth-$\ell$ part is exactly the
|
||||
annular faces of $\mathsf{K}_{\ell}$ at $v$, the depth-$(\ell-1)$ part
|
||||
those of $\mathsf{K}_{\ell-1}$, and summing $\lambda$ over the two parts
|
||||
gives the identity; \eqref{eq:heawood-face-sum-dual} is its vanishing.
|
||||
\end{proof}
|
||||
|
||||
\begin{remark}[Failure at the tire level]
|
||||
\label{rem:why-clusters}
|
||||
Proposition~\ref{prop:two-sided-decomposition} is what makes the binary
|
||||
parent/child pairing possible, and it requires the cluster. A vertex
|
||||
$v$ may lie on many depth-$\ell$ tires --- the unbounded case of
|
||||
Section~\ref{sec:tire-clusters} --- and the per-tire value
|
||||
$\lambda^{\!*}(v)$ of Definition~\ref{def:heawood-labelling} then records
|
||||
only the faces of \emph{one} tire at $v$, a fragment of $v$'s face-star.
|
||||
No single child tire carries the complete depth-$\ell$ sum, so the label
|
||||
$\sum_{f \ni v}\lambda(f)$ cannot be written as one child value plus one
|
||||
parent value, and per-tire compatibility
|
||||
(Definition~\ref{def:heawood-compatible}) fails to assemble to
|
||||
\eqref{eq:heawood-face-sum-dual}. Clustering repairs this:
|
||||
Proposition~\ref{prop:two-clusters-per-vertex} guarantees exactly one
|
||||
cluster meets $v$ on each side, so $\lambda^{\!*}_{\mathsf{K}_{\ell}}(v)$
|
||||
is the complete child contribution and
|
||||
$\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v)$ the complete parent
|
||||
contribution. Every vertex label is then realised as the combination of
|
||||
a single child-cluster value with a single parent-cluster value, and the
|
||||
pigeonhole programme below chains \emph{nested connected tire clusters}
|
||||
rather than individual tires.
|
||||
\end{remark}
|
||||
|
||||
We write $R_{\mathsf{K}}$ for the \emph{cluster Heawood restriction
|
||||
relation}: the set of (outer, inner) boundary Heawood sequence pairs
|
||||
realisable by a face-labelling of $\mathsf{K}$, defined as in
|
||||
Definition~\ref{def:boundary-sequences} but with the outer and inner
|
||||
boundaries of the cluster and the complete one-sided values
|
||||
$\lambda^{\!*}_{\mathsf{K}}$ in place of a single tire's, read up to
|
||||
rotation and global sign-flip. By
|
||||
Proposition~\ref{prop:two-sided-decomposition} two nested clusters are
|
||||
compatible along their shared interface exactly when the inner sequence
|
||||
of the parent is the pointwise negation mod $3$ of the outer sequence of
|
||||
the child (after the orientation reversal of
|
||||
Definition~\ref{def:heawood-compatible}).
|
||||
|
||||
\begin{conjecture}[Heawood chain-pigeonhole principle]
|
||||
\label{conj:heawood-chain-pigeonhole}
|
||||
There is a function $N(k)$ such that the following holds. Let
|
||||
\[
|
||||
\mathsf{K}_0 \supset \mathsf{K}_1 \supset \cdots \supset
|
||||
\mathsf{K}_{N(k)}
|
||||
\]
|
||||
be a nested chain of connected tire clusters in $\mathcal{T}(G, S)$ whose
|
||||
shared interfaces have length at most $k$. Then two adjacent cluster
|
||||
restriction relations $R_{\mathsf{K}_i}, R_{\mathsf{K}_{i+1}}$ in the
|
||||
chain admit compatible face-labellings along their shared interface,
|
||||
after rotation and global sign-flip. Equivalently, the chain contains a
|
||||
local gluing step that cannot be obstructed by disjoint Heawood boundary
|
||||
restrictions.
|
||||
\end{conjecture}
|
||||
|
||||
\begin{conjecture}[Heawood cluster route to the Four Colour Theorem]
|
||||
\label{conj:heawood-route-fct}
|
||||
For every plane triangulation $G$ and every level source $S$, the
|
||||
cluster Heawood restriction relations
|
||||
$\{R_{\mathsf{K}} : \mathsf{K} \text{ a connected tire cluster}\}$ admit
|
||||
a selection of face-labellings that is compatible along every cluster
|
||||
interface. By Proposition~\ref{prop:two-sided-decomposition} and
|
||||
Remark~\ref{rem:compat-is-heawood} this yields a $\{+1,-1\}$
|
||||
face-labelling of $G$ satisfying \eqref{eq:heawood-face-sum-dual}, hence
|
||||
$G$ is properly $4$-vertex-colourable.
|
||||
\end{conjecture}
|
||||
|
||||
%% TODO: realisability of $R_{\mathsf{K}}$ per cluster; counting /
|
||||
%% pigeonhole bound giving $N(k)$; orientation/reversal bookkeeping on
|
||||
%% the shared interface.
|
||||
|
||||
\section{The constraint floor}
|
||||
\label{sec:constraint-floor}
|
||||
|
||||
A nested substructure constrains its outer interface through the set of
|
||||
Heawood boundary sequences it can realise. By the self-similarity of the
|
||||
tire decomposition (\cite{bauerfeld-nested-tires}), the region $G_T$
|
||||
enclosed by a tire's outer cycle, away from the source, is itself a
|
||||
triangulated disk; we ask how tightly any such disk can constrain its
|
||||
boundary. The achievable set below depends only on the disk
|
||||
triangulation, not on a tire-tree labelling.
|
||||
|
||||
\begin{definition}[Achievable boundary set of a disk]
|
||||
\label{def:achievable-boundary-set}
|
||||
Let $D$ be a triangulated disk whose boundary is a simple $n$-cycle
|
||||
$C = (v_0, \dots, v_{n-1})$. Call a Heawood face-labelling
|
||||
$\lambda : F(D) \to \{+1,-1\}$ \emph{interior-valid} if
|
||||
$\sum_{f \ni w} \lambda(f) \equiv 0 \pmod 3$ at every interior vertex $w$
|
||||
of $D$ (no condition on $C$). The \emph{achievable boundary set} of $D$
|
||||
is
|
||||
\[
|
||||
\Phi(D) \;:=\; \bigl\{\,
|
||||
(\lambda^{*}(v_0), \dots, \lambda^{*}(v_{n-1}))
|
||||
\;:\; \lambda \text{ interior-valid} \,\bigr\}
|
||||
\;\subseteq\; \{0,1,-1\}^{n} .
|
||||
\]
|
||||
\end{definition}
|
||||
|
||||
\begin{proposition}[Interior-free disks attain $2^{n-2}$]
|
||||
\label{prop:attainment}
|
||||
If $D$ has no interior vertices then $|\Phi(D)| = 2^{\,n-2}$.
|
||||
\end{proposition}
|
||||
|
||||
\begin{proof}
|
||||
A triangulation of the $n$-gon has an \emph{ear}: a face
|
||||
$(v_{i-1}, v_i, v_{i+1})$ whose middle vertex $v_i$ has face-degree $1$,
|
||||
so $\lambda^{*}(v_i)$ equals that face's label and is read directly off
|
||||
the boundary sequence. Deleting the ear leaves a triangulation of the
|
||||
$(n-1)$-gon inducing the restricted boundary sequence; inducting, the
|
||||
$n-2$ face labels are recovered injectively, so $\lambda \mapsto
|
||||
\lambda^{*}|_C$ is a bijection onto a set of size $2^{\,n-2}$.
|
||||
\end{proof}
|
||||
|
||||
\begin{lemma}[Un-stacking]
|
||||
\label{lem:unstack}
|
||||
If $v$ is a degree-$3$ interior vertex of $D$, deleting it and restoring
|
||||
its link triangle as a single face yields a disk $D'$ with one fewer
|
||||
interior vertex and $\Phi(D') = \Phi(D)$.
|
||||
\end{lemma}
|
||||
|
||||
\begin{proof}
|
||||
The constraint at $v$ forces its three faces to a common value $s$,
|
||||
contributing $2s \equiv -s$ to each of the three link vertices; the
|
||||
restored triangle, labelled $-s$, reproduces that contribution at each,
|
||||
and $s \mapsto -s$ is a bijection on $\{+1,-1\}$. The resulting map is a
|
||||
bijection between interior-valid labellings of $D$ and of $D'$ preserving
|
||||
every boundary value, hence $\Phi(D) = \Phi(D')$.
|
||||
\end{proof}
|
||||
|
||||
\begin{conjecture}[Constraint floor]
|
||||
\label{conj:constraint-floor}
|
||||
For every triangulated disk $D$ with boundary an $n$-cycle,
|
||||
$|\Phi(D)| \ge 2^{\,n-2}$. Equivalently, no nested structure constrains
|
||||
the outer cycle below $2^{\,n-2}$ achievable Heawood sequences.
|
||||
\end{conjecture}
|
||||
|
||||
\begin{remark}[Status of Conjecture~\ref{conj:constraint-floor}]
|
||||
\label{rem:floor-status}
|
||||
Iterating Lemma~\ref{lem:unstack} reduces any disk, $\Phi$-faithfully and
|
||||
at fixed $n$, to one with no degree-$3$ interior vertex: either
|
||||
interior-free, where Proposition~\ref{prop:attainment} gives exactly
|
||||
$2^{\,n-2}$, or \emph{irreducible} (every interior vertex of degree
|
||||
$\ge 4$). Thus Conjecture~\ref{conj:constraint-floor} holds for the
|
||||
entire stacked (Apollonian) class and reduces to the irreducible case.
|
||||
Empirically it holds without exception over more than $10^4$ disks, and
|
||||
\emph{strictly}: every irreducible disk satisfies $|\Phi(D)| \ge
|
||||
\tfrac54 \cdot 2^{\,n-2}$, with equality at a single minimal-degree
|
||||
interior vertex; the wheel $W_n$ gives $|\Phi(W_n)| = \lfloor 2^n/3
|
||||
\rfloor$ and is \emph{not} the minimiser. A counting balance makes the
|
||||
floor plausible --- a disk with $k$ interior vertices has $2k+n-2$ faces
|
||||
(Euler) but only $k$ interior constraints, so the linear free dimension
|
||||
$k+n-2$ grows with depth --- but this is only heuristic: $|\Phi(D)|$ is
|
||||
\emph{not} monotone in $k$ (inserting a degree-$4$ vertex can shrink it),
|
||||
it merely never drops below $2^{\,n-2}$. Two natural elementary proofs,
|
||||
a $\Phi$-non-increasing vertex reduction and a direct $(n{-}2)$-face
|
||||
transversal, both provably fail; a proof appears to need a global
|
||||
argument on the Boolean / mod-$3$ structure of $\Phi$.
|
||||
\end{remark}
|
||||
|
||||
\begin{remark}
|
||||
\label{rem:floor-consequences}
|
||||
Two consequences. First, $\Phi(D)$ is a $\mathbb{Z}/3$ zonotope --- a
|
||||
projected cube, sign-closed but not a $\mathrm{GF}(3)$ subspace --- and at
|
||||
the interior-free value it has size $2^{\,n-2}$ with affine hull of
|
||||
dimension $n-2$. Second, granting Conjecture~\ref{conj:constraint-floor},
|
||||
the floor is exponential in the interface length $n$, so a
|
||||
maximally-constraining child still offers $2^{\,n-2}$ outer options, and
|
||||
the gluing of Conjecture~\ref{conj:heawood-chain-pigeonhole} has the least
|
||||
slack at \emph{short} interfaces (e.g.\ $n = 4$ leaves $4$ options) and is
|
||||
easy at long ones; the difficulty of the programme is concentrated at
|
||||
short level cycles.
|
||||
\end{remark}
|
||||
|
||||
\begin{thebibliography}{9}
|
||||
|
||||
\bibitem{Heawood1898}
|
||||
P.~J.~Heawood,
|
||||
\emph{On the four-colour map theorem},
|
||||
Quart. J.~Pure Appl. Math. \textbf{29} (1898), 270--285.
|
||||
|
||||
\bibitem{bauerfeld-depth}
|
||||
E.~Bauerfeld,
|
||||
\emph{Plane Depth},
|
||||
manuscript (math-research repository), 2026.
|
||||
|
||||
\bibitem{bauerfeld-nested-tires}
|
||||
E.~Bauerfeld,
|
||||
\emph{Nested Tire Decompositions of Plane Triangulations},
|
||||
manuscript (math-research repository), 2026.
|
||||
|
||||
\bibitem{bauerfeld-medial-tires}
|
||||
E.~Bauerfeld,
|
||||
\emph{Medial Tire Decompositions of Plane Triangulations},
|
||||
manuscript (math-research repository), 2026.
|
||||
|
||||
\bibitem{bauerfeld-nested-tire-duals}
|
||||
E.~Bauerfeld,
|
||||
\emph{Coloring Nested Tire Dual Graphs},
|
||||
manuscript (math-research repository), 2026.
|
||||
|
||||
\end{thebibliography}
|
||||
|
||||
\end{document}
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 297 KiB |
@@ -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
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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 face: (8, 9, 19)
|
||||
- chosen source cap vertex: 24
|
||||
- root entry tooth: e2 (apex medial vertex = level-1 edge (19, 8))
|
||||
- recognised treads: 11
|
||||
- skipped treads: [((0, 0), 'only 0 up teeth')]
|
||||
- removed source-dual edges: 29
|
||||
- annular/cap cuts: 12
|
||||
- up-apex cuts: 17
|
||||
|
||||
- **source-dual cut is a tree: True** (56 dual faces, 55 edges, 1 component(s), acyclic=True)
|
||||
|
||||
## Walk-distance labelling of the source-dual cut
|
||||
|
||||
Each dual face (vertex of the source-dual cut) is labelled by its distance, within the cut, from the **cap down tooth of the first entry**: the triangular face `(8, 24, 19)` = `{source 24, edge (19, 8)}` (dual node 34).
|
||||
|
||||
- maximum distance: 21
|
||||
- distance histogram (faces by distance): `{0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 2, 6: 3, 7: 3, 8: 4, 9: 4, 10: 4, 11: 4, 12: 3, 13: 3, 14: 2, 15: 2, 16: 1, 17: 2, 18: 3, 19: 4, 20: 2, 21: 1}`
|
||||
|
||||
- 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)]`
|
||||
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 458 KiB |
@@ -0,0 +1,39 @@
|
||||
# Full medial tire cut walk 2
|
||||
|
||||
- base vertices: 20
|
||||
- deep-embedded vertices: 21
|
||||
- deep-embedded edges: 57
|
||||
- graph seed: 2
|
||||
- deep-embedded minimum degree: 3
|
||||
- chosen face: (4, 12, 11)
|
||||
- chosen source cap vertex: 20
|
||||
- root entry tooth: e3 (apex medial vertex = level-1 edge (11, 4))
|
||||
- recognised treads: 3
|
||||
- skipped treads: [((0, 0), 'only 0 up teeth')]
|
||||
- removed source-dual edges: 20
|
||||
- annular/cap cuts: 5
|
||||
- up-apex cuts: 15
|
||||
|
||||
- **source-dual cut is a tree: True** (38 dual faces, 37 edges, 1 component(s), acyclic=True)
|
||||
|
||||
## Walk-distance labelling of the source-dual cut
|
||||
|
||||
Each dual face (vertex of the source-dual cut) is labelled by its distance, within the cut, from the **cap down tooth of the first entry**: the triangular face `(4, 20, 11)` = `{source 20, edge (11, 4)}` (dual node 15).
|
||||
|
||||
- maximum distance: 17
|
||||
- distance histogram (faces by distance): `{0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 3, 7: 3, 8: 3, 9: 3, 10: 4, 11: 3, 12: 2, 13: 1, 14: 1, 15: 1, 16: 1, 17: 1}`
|
||||
|
||||
- dual cut figure: `full_medial_tire_cut_walk_2_dual.png`
|
||||
- tire cut grid: `full_medial_tire_cut_walk_2_tires.png`
|
||||
- combined PDF: `full_medial_tire_cut_walk_2.pdf`
|
||||
|
||||
| tread | depth | component | annular | up | singleton down | bite apexes | entry | closing cuts | up-apex cuts | shared/entry skipped |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| T1 | 1 | 0 | 10 | 3 | 7 | 0 | e3 | 1 | 2 | 1 |
|
||||
| T2 | 2 | 0 | 15 | 7 | 8 | 0 | e3 | 1 | 6 | 1 |
|
||||
| T3 | 3 | 0 | 10 | 8 | 0 | 1 | e7 | 2 | 7 | 1 |
|
||||
|
||||
## Removed Source-Dual Edges
|
||||
|
||||
- annular/cap: `[(3, 4), (3, 9), (8, 9), (11, 20), (15, 7)]`
|
||||
- up apexes: `[(0, 3), (0, 5), (1, 2), (1, 6), (2, 9), (10, 18), (11, 12), (12, 4), (13, 5), (13, 19), (14, 6), (14, 15), (15, 16), (16, 17), (18, 19)]`
|
||||
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,200 @@
|
||||
"""Regenerate the ``full_walk`` contents (``.md`` report + ``_dual.png`` +
|
||||
``_tires.png`` + combined ``.pdf``) for each configured medial tire cut walk.
|
||||
|
||||
Each walk fixes a reproducible source: a base maximal planar 5-connected graph
|
||||
``random_maximal_planar_5_connected(n, seed)``, a chosen triangular face, the
|
||||
deep-embedding cap vertex placed inside that face as the level source, and a
|
||||
root entry tooth. The source-dual cut, its walk-distance labelling, and the
|
||||
figures are all read off the deep embedding ``G'`` (whose dual-cut figure is now
|
||||
drawn on a *valid straight-line embedding* of ``G'`` -- see
|
||||
``medial_tire_dual_cut_experiment._straight_line_source_layout``).
|
||||
|
||||
Run with the repo venv::
|
||||
|
||||
../../../.venv/bin/python generate_full_walk.py # all walks
|
||||
../../../.venv/bin/python generate_full_walk.py --walk 1 # just walk 1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import networkx as nx
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_EXP = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _EXP)
|
||||
|
||||
import medial_tire_dual_cut_experiment as E # noqa: E402
|
||||
from run_medial_tire_cut_experiment import ( # noqa: E402
|
||||
random_maximal_planar_5_connected,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Walk configurations. Each is fully reproducible from (n, seed, face, entry).
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
WALKS = [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "Full medial tire cut walk 1",
|
||||
"n": 20,
|
||||
"seed": 59,
|
||||
"face": (8, 9, 19),
|
||||
"entry": 2,
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"title": "Full medial tire cut walk 2",
|
||||
"n": 20,
|
||||
"seed": 2,
|
||||
"face": (4, 12, 11),
|
||||
"entry": 3,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def build_result(cfg):
|
||||
"""Build the source-dual cut result dict for one walk configuration."""
|
||||
G, graph_seed = random_maximal_planar_5_connected(
|
||||
cfg["n"], cfg["seed"], min_connectivity=5)
|
||||
G_prime, cap, depth = E.deep_embedding(G, cfg["face"])
|
||||
result = E.medial_tire_dual_cut(G_prime, cap, cfg["entry"])
|
||||
result["base_graph"] = G
|
||||
result["chosen_face"] = tuple(cfg["face"])
|
||||
result["cap_vertex"] = cap
|
||||
result["deep_depth"] = depth
|
||||
result["graph_seed"] = graph_seed
|
||||
result["base_min_degree"] = min(dict(G.degree()).values())
|
||||
result["base_connectivity"] = nx.node_connectivity(G)
|
||||
result["min_degree"] = min(dict(G_prime.degree()).values())
|
||||
result["connectivity"] = nx.node_connectivity(G_prime)
|
||||
return result
|
||||
|
||||
|
||||
def _tree_report(result):
|
||||
"""``(is_tree, n_faces, n_edges, n_components, acyclic)`` for the cut."""
|
||||
cut = E.dual_cut_graph(result)
|
||||
n = cut.number_of_nodes()
|
||||
e = cut.number_of_edges()
|
||||
comps = nx.number_connected_components(cut)
|
||||
return nx.is_tree(cut), n, e, comps, (e == n - comps)
|
||||
|
||||
|
||||
def _distance_histogram(dist):
|
||||
"""Histogram of dual faces by walk distance, as an ordered dict."""
|
||||
hist = {}
|
||||
for d in dist.values():
|
||||
hist[d] = hist.get(d, 0) + 1
|
||||
return {k: hist[k] for k in sorted(hist)}
|
||||
|
||||
|
||||
def render_markdown(result, cfg):
|
||||
"""The walk report in the committed ``.md`` format."""
|
||||
G = result["G"]
|
||||
base = result["base_graph"]
|
||||
removed = result["removed_dual_edges"]
|
||||
res = result["results"]
|
||||
i = cfg["index"]
|
||||
em = result["entry_medial_vertex"]
|
||||
|
||||
is_tree, n_faces, n_edges, comps, acyclic = _tree_report(result)
|
||||
dist, root = E.dual_cut_distances(result)
|
||||
root_face = result["faces"][root]
|
||||
hist = _distance_histogram(dist)
|
||||
max_dist = max(dist.values()) if dist else 0
|
||||
|
||||
lines = [
|
||||
f"# {cfg['title']}",
|
||||
"",
|
||||
f"- base vertices: {base.number_of_nodes()}",
|
||||
f"- deep-embedded vertices: {G.number_of_nodes()}",
|
||||
f"- deep-embedded edges: {G.number_of_edges()}",
|
||||
f"- graph seed: {result['graph_seed']}",
|
||||
f"- deep-embedded minimum degree: {result['min_degree']}",
|
||||
f"- chosen face: {result['chosen_face']}",
|
||||
f"- chosen source cap vertex: {result['source']}",
|
||||
f"- root entry tooth: e{result['entry_edge']} "
|
||||
f"(apex medial vertex = level-1 edge {em})",
|
||||
f"- recognised treads: {len(res)}",
|
||||
f"- skipped treads: {result['skipped']}",
|
||||
f"- removed source-dual edges: {len(removed)}",
|
||||
f"- annular/cap cuts: {len(result['annular_cut_edges'])}",
|
||||
f"- up-apex cuts: {len(result['apex_cut_edges'])}",
|
||||
"",
|
||||
f"- **source-dual cut is a tree: {is_tree}** ({n_faces} dual faces, "
|
||||
f"{n_edges} edges, {comps} component(s), acyclic={acyclic})",
|
||||
"",
|
||||
"## Walk-distance labelling of the source-dual cut",
|
||||
"",
|
||||
"Each dual face (vertex of the source-dual cut) is labelled by its "
|
||||
"distance, within the cut, from the **cap down tooth of the first "
|
||||
f"entry**: the triangular face `{root_face}` = "
|
||||
f"`{{source {result['source']}, edge {em}}}` (dual node {root}).",
|
||||
"",
|
||||
f"- maximum distance: {max_dist}",
|
||||
f"- distance histogram (faces by distance): `{hist}`",
|
||||
"",
|
||||
f"- dual cut figure: `full_medial_tire_cut_walk_{i}_dual.png`",
|
||||
f"- tire cut grid: `full_medial_tire_cut_walk_{i}_tires.png`",
|
||||
f"- combined PDF: `full_medial_tire_cut_walk_{i}.pdf`",
|
||||
"",
|
||||
"| tread | depth | component | annular | up | singleton down | "
|
||||
"bite apexes | entry | closing cuts | up-apex cuts | "
|
||||
"shared/entry skipped |",
|
||||
"|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|",
|
||||
]
|
||||
for idx, key in enumerate(sorted(res), start=1):
|
||||
d, comp = key
|
||||
rec = res[key]
|
||||
g, bij, entry = rec["g"], rec["bij"], rec["entry_edge"]
|
||||
up = len(g.up_edges)
|
||||
apex = len(E.up_apex_cuts(g, entry, bij))
|
||||
lines.append(
|
||||
f"| T{idx} | {d} | {comp} | {g.n} | {up} | "
|
||||
f"{len(g.singleton_down_edges)} | {len(g.bites)} | e{entry} | "
|
||||
f"{len(rec['cuts'])} | {apex} | {up - apex} |")
|
||||
lines += [
|
||||
"",
|
||||
"## Removed Source-Dual Edges",
|
||||
"",
|
||||
f"- annular/cap: `{sorted(result['annular_cut_edges'])}`",
|
||||
f"- up apexes: `{sorted(result['apex_cut_edges'])}`",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate(cfg):
|
||||
"""Build one walk and write its ``.md`` report and three figures."""
|
||||
result = build_result(cfg)
|
||||
i = cfg["index"]
|
||||
stem = os.path.join(_HERE, f"full_medial_tire_cut_walk_{i}")
|
||||
|
||||
with open(f"{stem}.md", "w") as fh:
|
||||
fh.write(render_markdown(result, cfg))
|
||||
E.draw_png(result, f"{stem}_dual.png")
|
||||
E.draw_tire_cuts_png(result, f"{stem}_tires.png")
|
||||
E.draw_combined_pdf(result, f"{stem}.pdf")
|
||||
|
||||
print(f"walk {i}: wrote {stem}.md + _dual.png + _tires.png + .pdf")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--walk", type=int, default=None,
|
||||
help="regenerate only this walk index (default: all)")
|
||||
args = parser.parse_args()
|
||||
|
||||
for cfg in WALKS:
|
||||
if args.walk is None or cfg["index"] == args.walk:
|
||||
generate(cfg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 63 KiB |
@@ -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__":
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
End-to-end experiment for the *Medial Tire Cuts* paper:
|
||||
|
||||
1. Generate a random maximal planar graph G on n vertices (stacked seed plus
|
||||
random diagonal flips; ``random_maximal_planar`` from the medial tire
|
||||
decompositions experiments), optionally rejecting samples below a requested
|
||||
minimum degree.
|
||||
1. Generate a 5-connected maximal planar graph G on n vertices, using
|
||||
``plantri -c5`` when available and verifying node connectivity.
|
||||
2. Build its medial graph M(G).
|
||||
3. Take the nested tire decomposition at one random vertex level source: the
|
||||
BFS-level treads, each realized as a FullMedialTireGraph.
|
||||
@@ -40,6 +38,7 @@ import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import networkx as nx
|
||||
|
||||
@@ -49,16 +48,132 @@ _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,
|
||||
ekey, medial_graph, medial_tire_facemodel,
|
||||
random_maximal_planar, recognise, triangular_faces,
|
||||
)
|
||||
from medial_tire_cut_labelling import door_bite, label_and_cut # noqa: E402
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Component-based tread recognition.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _edge_face_data(faces):
|
||||
edge_faces = defaultdict(list)
|
||||
for i, face in enumerate(faces):
|
||||
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
|
||||
edge_faces[ekey(x, y)].append(i)
|
||||
return edge_faces
|
||||
|
||||
|
||||
def _depth_components(faces, edge_faces, levels):
|
||||
depths = [min(levels[v] for v in face) for face in faces]
|
||||
dual_adj = 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])
|
||||
|
||||
comps = []
|
||||
seen = [False] * len(faces)
|
||||
for start in range(len(faces)):
|
||||
if seen[start]:
|
||||
continue
|
||||
depth = depths[start]
|
||||
stack = [start]
|
||||
comp = []
|
||||
seen[start] = True
|
||||
while stack:
|
||||
face = stack.pop()
|
||||
comp.append(face)
|
||||
for other in dual_adj[face]:
|
||||
if not seen[other] and depths[other] == depth:
|
||||
seen[other] = True
|
||||
stack.append(other)
|
||||
comps.append((depth, tuple(sorted(comp))))
|
||||
return comps
|
||||
|
||||
|
||||
def _tread_from_component(faces, levels, face_indices):
|
||||
tread_faces = [faces[i] for i in face_indices]
|
||||
if not tread_faces:
|
||||
return None
|
||||
depth = min(min(levels[v] for v in face) for face in tread_faces)
|
||||
annular, up, down = set(), set(), set()
|
||||
face_of_down = defaultdict(int)
|
||||
for face in tread_faces:
|
||||
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
|
||||
e = ekey(x, y)
|
||||
lx, ly = levels[x], levels[y]
|
||||
if {lx, ly} == {depth, depth + 1}:
|
||||
annular.add(e)
|
||||
elif lx == ly == depth:
|
||||
up.add(e)
|
||||
elif lx == ly == depth + 1:
|
||||
down.add(e)
|
||||
face_of_down[e] += 1
|
||||
if len(annular) < 3:
|
||||
return None
|
||||
return {
|
||||
"tread_faces": tread_faces,
|
||||
"annular": annular,
|
||||
"up": up,
|
||||
"down": down,
|
||||
"bites": {e for e in down if face_of_down[e] == 2},
|
||||
}
|
||||
|
||||
|
||||
def _build_treads(faces, levels):
|
||||
"""Recognise simple cycles inside connected depth components.
|
||||
|
||||
The returned ``treads`` keeps the existing simple-tire interface used by
|
||||
the labelling code. ``tread_meta`` records the connected depth component
|
||||
each simple cycle came from, so compound tires can be chained through
|
||||
shared up apexes rather than seeded as unrelated roots.
|
||||
"""
|
||||
treads, skipped, tread_meta = {}, [], {}
|
||||
edge_faces = _edge_face_data(faces)
|
||||
comps = sorted(_depth_components(faces, edge_faces, levels),
|
||||
key=lambda item: (item[0], item[1]))
|
||||
component_count = defaultdict(int)
|
||||
tire_count = defaultdict(int)
|
||||
for depth, face_indices in comps:
|
||||
component = component_count[depth]
|
||||
component_count[depth] += 1
|
||||
tread = _tread_from_component(faces, levels, face_indices)
|
||||
if tread is None:
|
||||
skipped.append(((depth, component), "no tread faces"))
|
||||
continue
|
||||
if len(tread["up"]) < 3:
|
||||
skipped.append(((depth, component), f"only {len(tread['up'])} up teeth"))
|
||||
continue
|
||||
tires = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
|
||||
if not tires:
|
||||
skipped.append(((depth, component), "no annular cycle recognised as a tire"))
|
||||
continue
|
||||
compound = (depth, component)
|
||||
cycle_count = len(tires)
|
||||
for cycle, gb in enumerate(tires):
|
||||
key = (depth, tire_count[depth])
|
||||
tire_count[depth] += 1
|
||||
treads[key] = gb
|
||||
tread_meta[key] = {
|
||||
"compound": compound,
|
||||
"cycle": cycle,
|
||||
"cycle_count": cycle_count,
|
||||
"face_indices": face_indices,
|
||||
}
|
||||
return treads, skipped, tread_meta
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 4. Walk-depth labelling and cut, chained down the tire tree.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -68,31 +183,113 @@ def _apex_vertex(g, bij, edge):
|
||||
return bij[g.apex_of_edge(edge)]
|
||||
|
||||
|
||||
def _label_treads(treads, results):
|
||||
"""Fill ``results[d]`` with the walk-depth labelling and cuts for each
|
||||
recognised tread ``d``, chaining child entries to parent down teeth."""
|
||||
for d in sorted(treads):
|
||||
g, bij = treads[d]
|
||||
parent = treads.get(d - 1)
|
||||
if parent is None:
|
||||
entry_edge, start_depth = g.up_edges[0], 0 # arbitrary root entry
|
||||
else:
|
||||
pg, pbij = parent
|
||||
pdepth = results[d - 1]["depth"]
|
||||
# parent down teeth, lowest walk depth first
|
||||
down = sorted((pdepth[k], _apex_vertex(pg, pbij, k))
|
||||
for k in pg.down_edges)
|
||||
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
|
||||
entry_edge = start_depth = None
|
||||
for value, apex in down:
|
||||
if apex in child_up_apex:
|
||||
entry_edge, start_depth = child_up_apex[apex], value + 1
|
||||
break
|
||||
if entry_edge is None: # no shared apex (degenerate); root-style
|
||||
entry_edge, start_depth = g.up_edges[0], 0
|
||||
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||
results[d] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||
"start_depth": start_depth, "depth": depth, "cuts": cuts}
|
||||
def _label_treads(treads, results, root_entry_edge=None, tread_meta=None):
|
||||
"""Fill ``results[(d, c)]`` with the walk-depth labelling and cuts for every
|
||||
recognised tire ``c`` of every tread depth ``d``, chaining child entries to
|
||||
parent down teeth.
|
||||
|
||||
``treads`` maps ``(depth, component)`` -> ``(g, bij)``; a tread depth may
|
||||
carry several tires (one per disjoint annular cycle). The root tire
|
||||
``(root_d, 0)`` is entered at ``root_entry_edge`` when given -- it must be
|
||||
one of that tire's up teeth -- otherwise at an arbitrary up tooth. Each
|
||||
other tire chains to whichever parent-depth down tooth (across all parent
|
||||
tires) shares its apex, at the lowest parent walk depth.
|
||||
"""
|
||||
if not treads:
|
||||
return
|
||||
depths = sorted({k[0] for k in treads})
|
||||
root_d = depths[0]
|
||||
for d in depths:
|
||||
# apex medial vertex -> child start depth, over all parent-depth tires
|
||||
parent_down = {}
|
||||
for pk in (k for k in treads if k[0] == d - 1):
|
||||
pg, pbij = treads[pk]
|
||||
pdepth = results[pk]["depth"]
|
||||
for e in pg.down_edges:
|
||||
apex = _apex_vertex(pg, pbij, e)
|
||||
value = pdepth[e] + 1
|
||||
if apex not in parent_down or value < parent_down[apex]:
|
||||
parent_down[apex] = value
|
||||
has_parent = any(k[0] == d - 1 for k in treads)
|
||||
pending = sorted(k for k in treads if k[0] == d)
|
||||
while pending:
|
||||
progressed = False
|
||||
deferred = []
|
||||
use_sibling_entries = has_parent and not any(
|
||||
parent_down.keys() & {treads[key][1][f"u{m}"]
|
||||
for m in treads[key][0].up_edges}
|
||||
for key in pending
|
||||
if key not in results
|
||||
)
|
||||
for key in pending:
|
||||
if key in results:
|
||||
continue
|
||||
g, bij = treads[key]
|
||||
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
|
||||
entry = None
|
||||
if not has_parent:
|
||||
if (key == (root_d, 0) and root_entry_edge is not None
|
||||
and root_entry_edge in g.up_edges):
|
||||
entry = (root_entry_edge, 0)
|
||||
else:
|
||||
entry = (g.up_edges[0], 0) # arbitrary entry
|
||||
else:
|
||||
best = None
|
||||
for apex, value in parent_down.items():
|
||||
if apex in child_up_apex and (best is None or value < best[1]):
|
||||
best = (child_up_apex[apex], value)
|
||||
if best is not None: # chains to a parent down tooth
|
||||
entry = best
|
||||
elif use_sibling_entries:
|
||||
compound = (
|
||||
tread_meta.get(key, {}).get("compound")
|
||||
if tread_meta is not None else None
|
||||
)
|
||||
sibling_best = None
|
||||
if compound is not None:
|
||||
for sk, srec in results.items():
|
||||
if sk[0] != d or srec.get("compound") != compound:
|
||||
continue
|
||||
sg, sbij = srec["g"], srec["bij"]
|
||||
for edge in sg.up_edges:
|
||||
apex = sbij[f"u{edge}"]
|
||||
if apex not in child_up_apex:
|
||||
continue
|
||||
value = srec["depth"][edge] + 1
|
||||
if (sibling_best is None
|
||||
or value > sibling_best[1]):
|
||||
sibling_best = (child_up_apex[apex], value)
|
||||
if sibling_best is not None:
|
||||
entry = sibling_best
|
||||
|
||||
if entry is None:
|
||||
deferred.append(key)
|
||||
continue
|
||||
entry_edge, start_depth = entry
|
||||
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||
"start_depth": start_depth, "depth": depth,
|
||||
"cuts": cuts}
|
||||
if tread_meta is not None and key in tread_meta:
|
||||
results[key].update(tread_meta[key])
|
||||
progressed = True
|
||||
|
||||
if progressed:
|
||||
pending = deferred
|
||||
continue
|
||||
|
||||
# Degenerate component: no parent or labelled sibling gives an
|
||||
# entry. Seed it so any remaining sibling cycles can chain to it.
|
||||
key = deferred[0]
|
||||
g, bij = treads[key]
|
||||
entry_edge, start_depth = g.up_edges[0], 0
|
||||
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
|
||||
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
|
||||
"start_depth": start_depth, "depth": depth,
|
||||
"cuts": cuts}
|
||||
if tread_meta is not None and key in tread_meta:
|
||||
results[key].update(tread_meta[key])
|
||||
pending = deferred[1:]
|
||||
|
||||
|
||||
def _cap_cut(G, emb, source, levels, results):
|
||||
@@ -173,21 +370,22 @@ def _assemble_cut_graph(M, results, cap_cuts=None):
|
||||
**{v: copy_a for v in c["neighbours_a"]},
|
||||
**{v: copy_b for v in c["neighbours_b"]},
|
||||
}
|
||||
for d in sorted(results):
|
||||
g, bij = results[d]["g"], results[d]["bij"]
|
||||
for key in sorted(results):
|
||||
td = key[0]
|
||||
g, bij = results[key]["g"], results[key]["bij"]
|
||||
n = g.n
|
||||
for c in results[d]["cuts"]:
|
||||
for c in results[key]["cuts"]:
|
||||
kk = c.vertex
|
||||
if kk is None:
|
||||
continue
|
||||
mv = bij[f"a{kk}"]
|
||||
if mv in split:
|
||||
warnings.append(f"annular vertex a{kk} of tread {d} cut twice; "
|
||||
warnings.append(f"annular vertex a{kk} of tread {key} cut twice; "
|
||||
f"second cut not applied")
|
||||
continue
|
||||
e_prev, e_next = (kk - 1) % n, kk
|
||||
copy_a = (mv, "A", d)
|
||||
copy_b = (mv, "B", d)
|
||||
copy_a = (mv, "A", td)
|
||||
copy_b = (mv, "B", td)
|
||||
split[mv] = {
|
||||
bij[f"a{(kk - 1) % n}"]: copy_a,
|
||||
_apex_vertex(g, bij, e_prev): copy_a,
|
||||
@@ -206,13 +404,14 @@ def _assemble_cut_graph(M, results, cap_cuts=None):
|
||||
H.add_edge(resolve(u, v), resolve(v, u))
|
||||
|
||||
label_records = []
|
||||
for d in sorted(results):
|
||||
g, bij, depth = results[d]["g"], results[d]["bij"], results[d]["depth"]
|
||||
for key in sorted(results):
|
||||
td = key[0]
|
||||
g, bij, depth = results[key]["g"], results[key]["bij"], results[key]["depth"]
|
||||
for k in range(g.n):
|
||||
role = ("up" if g.tooth_word[k] == "U"
|
||||
else "bite" if door_bite(g, k) is not None else "down")
|
||||
label_records.append({
|
||||
"tread": d, "edge": k, "role": role,
|
||||
"tread": td, "comp": key[1], "edge": k, "role": role,
|
||||
"apex": _apex_vertex(g, bij, k), "walk": depth[k],
|
||||
})
|
||||
return H, label_records, warnings
|
||||
@@ -222,39 +421,46 @@ def _assemble_cut_graph(M, results, cap_cuts=None):
|
||||
# Driver.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def random_maximal_planar_min_degree(n: int, seed: int, flips: int = 400,
|
||||
min_degree: int = 0,
|
||||
attempts: int = 1000) -> tuple[nx.Graph, int]:
|
||||
"""Generate a random maximal planar graph with minimum degree at least
|
||||
``min_degree``. The returned seed is the actual sample seed used."""
|
||||
if min_degree <= 0:
|
||||
def random_maximal_planar_5_connected(n: int, seed: int, flips: int = 400,
|
||||
min_connectivity: int = 5,
|
||||
attempts: int = 1000) -> tuple[nx.Graph, int]:
|
||||
"""Generate a maximal planar graph with node connectivity at least
|
||||
``min_connectivity``. The returned seed is the actual sample seed used."""
|
||||
if min_connectivity <= 0:
|
||||
return random_maximal_planar(n, seed, flips=flips), seed
|
||||
|
||||
if min_degree >= 5:
|
||||
if min_connectivity >= 5:
|
||||
plantri = os.path.normpath(os.path.join(_HERE, "..", "..", "..",
|
||||
"plantri", "plantri"))
|
||||
if os.path.exists(plantri):
|
||||
data = subprocess.check_output(
|
||||
[plantri, f"-m{min_degree}", "-g", str(n)],
|
||||
[plantri, "-c5", "-g", str(n)],
|
||||
stderr=subprocess.DEVNULL)
|
||||
graphs = [line for line in data.splitlines() if line]
|
||||
graphs = [
|
||||
line for line in data.splitlines()
|
||||
if line and not line.startswith(b">>")
|
||||
]
|
||||
if graphs:
|
||||
G = nx.from_graph6_bytes(graphs[seed % len(graphs)])
|
||||
return nx.convert_node_labels_to_integers(G), seed
|
||||
for offset in range(len(graphs)):
|
||||
G = nx.from_graph6_bytes(graphs[(seed + offset) % len(graphs)])
|
||||
G = nx.convert_node_labels_to_integers(G)
|
||||
if nx.node_connectivity(G) >= min_connectivity:
|
||||
return G, seed + offset
|
||||
|
||||
for offset in range(attempts):
|
||||
sample_seed = seed + offset
|
||||
G = random_maximal_planar(n, sample_seed, flips=flips)
|
||||
if min(dict(G.degree()).values()) >= min_degree:
|
||||
if nx.node_connectivity(G) >= min_connectivity:
|
||||
return G, sample_seed
|
||||
raise RuntimeError(
|
||||
f"no random maximal planar graph on {n} vertices with "
|
||||
f"minimum degree >= {min_degree} found in {attempts} attempts "
|
||||
f"node connectivity >= {min_connectivity} found in {attempts} attempts "
|
||||
f"starting at seed {seed}")
|
||||
|
||||
|
||||
def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
|
||||
min_degree: int = 5, attempts: int = 1000) -> dict:
|
||||
min_connectivity: int = 5, attempts: int = 1000,
|
||||
min_degree: int | None = None) -> dict:
|
||||
"""Run the full pipeline and return a structured result.
|
||||
|
||||
Result keys: ``n, seed, G, M, source, treads`` (dict depth -> (g, bij)),
|
||||
@@ -262,37 +468,28 @@ def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
|
||||
(depth, reason)), ``cut_graph`` (networkx graph), ``labels`` (list of tooth
|
||||
records), ``warnings``.
|
||||
"""
|
||||
G, graph_seed = random_maximal_planar_min_degree(
|
||||
n, seed, flips=flips, min_degree=min_degree, attempts=attempts)
|
||||
if min_degree is not None:
|
||||
min_connectivity = max(min_connectivity, min_degree)
|
||||
G, graph_seed = random_maximal_planar_5_connected(
|
||||
n, seed, flips=flips, min_connectivity=min_connectivity, attempts=attempts)
|
||||
faces, emb = triangular_faces(G)
|
||||
M = medial_graph(G)
|
||||
source = random.Random(f"source-{graph_seed}").choice(sorted(G.nodes()))
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
|
||||
treads, skipped = {}, []
|
||||
for d in range(max(levels.values())):
|
||||
tread = extract_tread(faces, levels, d)
|
||||
if tread is None:
|
||||
skipped.append((d, "no tread faces"))
|
||||
continue
|
||||
if len(tread["up"]) < 3:
|
||||
skipped.append((d, f"only {len(tread['up'])} up teeth"))
|
||||
continue
|
||||
rec = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
|
||||
if rec is None:
|
||||
skipped.append((d, "not a valid full medial tire graph"))
|
||||
continue
|
||||
treads[d] = rec
|
||||
treads, skipped, tread_meta = _build_treads(faces, levels)
|
||||
|
||||
results = {}
|
||||
_label_treads(treads, results)
|
||||
_label_treads(treads, results, tread_meta=tread_meta)
|
||||
cap_cuts = _cap_cut(G, emb, source, levels, results)
|
||||
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
|
||||
return {
|
||||
"n": n, "seed": seed, "graph_seed": graph_seed,
|
||||
"min_degree": min(dict(G.degree()).values()),
|
||||
"connectivity": nx.node_connectivity(G),
|
||||
"G": G, "M": M, "source": source,
|
||||
"treads": treads, "results": results, "cap_cuts": cap_cuts,
|
||||
"treads": treads, "tread_meta": tread_meta,
|
||||
"results": results, "cap_cuts": cap_cuts,
|
||||
"skipped": skipped,
|
||||
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
|
||||
}
|
||||
@@ -313,9 +510,10 @@ def _vname(v) -> str:
|
||||
def to_json(result: dict) -> dict:
|
||||
res = result["results"]
|
||||
treads_out = []
|
||||
for d in sorted(res):
|
||||
g, bij = res[d]["g"], res[d]["bij"]
|
||||
depth, cuts = res[d]["depth"], res[d]["cuts"]
|
||||
for key in sorted(res):
|
||||
d, comp = key
|
||||
g, bij = res[key]["g"], res[key]["bij"]
|
||||
depth, cuts = res[key]["depth"], res[key]["cuts"]
|
||||
teeth = [{
|
||||
"edge": k,
|
||||
"role": ("up" if g.tooth_word[k] == "U"
|
||||
@@ -324,9 +522,9 @@ def to_json(result: dict) -> dict:
|
||||
"walk": depth[k],
|
||||
} for k in range(g.n)]
|
||||
treads_out.append({
|
||||
"depth": d, "n": g.n, "tooth_word": g.tooth_word,
|
||||
"depth": d, "comp": comp, "n": g.n, "tooth_word": g.tooth_word,
|
||||
"bites": sorted(list(b) for b in g.bites),
|
||||
"entry_edge": res[d]["entry_edge"], "start_depth": res[d]["start_depth"],
|
||||
"entry_edge": res[key]["entry_edge"], "start_depth": res[key]["start_depth"],
|
||||
"teeth": teeth,
|
||||
"cuts": [{
|
||||
"annular_index": c.vertex,
|
||||
@@ -338,6 +536,7 @@ def to_json(result: dict) -> dict:
|
||||
return {
|
||||
"n": result["n"], "seed": result["seed"],
|
||||
"graph_seed": result["graph_seed"], "min_degree": result["min_degree"],
|
||||
"connectivity": result["connectivity"],
|
||||
"source": result["source"],
|
||||
"graph_edges": sorted([int(u), int(v)] for u, v in result["G"].edges()),
|
||||
"medial_vertices": result["M"].number_of_nodes(),
|
||||
@@ -352,7 +551,8 @@ def to_json(result: dict) -> dict:
|
||||
"edges": sorted([_vname(u), _vname(v)] for u, v in H.edges()),
|
||||
},
|
||||
"labels": [{
|
||||
"tread": r["tread"], "edge": r["edge"], "role": r["role"],
|
||||
"tread": r["tread"], "comp": r.get("comp", 0),
|
||||
"edge": r["edge"], "role": r["role"],
|
||||
"apex": _vname(r["apex"]), "walk": r["walk"],
|
||||
} for r in result["labels"]],
|
||||
"warnings": result["warnings"],
|
||||
@@ -364,19 +564,21 @@ def summary(result: dict) -> str:
|
||||
lines = [
|
||||
f"random maximal planar graph: n={result['n']} requested seed={result['seed']} "
|
||||
f"graph seed={result['graph_seed']} "
|
||||
f"({result['G'].number_of_edges()} edges, min degree {result['min_degree']})",
|
||||
f"({result['G'].number_of_edges()} edges, "
|
||||
f"connectivity {result['connectivity']}, min degree {result['min_degree']})",
|
||||
f"medial graph M(G): {result['M'].number_of_nodes()} vertices",
|
||||
f"level source: vertex {result['source']}",
|
||||
f"recognised treads: {sorted(res)}",
|
||||
f"recognised tires (depth, component): {sorted(res)}",
|
||||
f"skipped treads: {result['skipped']}",
|
||||
]
|
||||
for d in sorted(res):
|
||||
g = res[d]["g"]
|
||||
ncuts = len(res[d]["cuts"])
|
||||
for key in sorted(res):
|
||||
d, comp = key
|
||||
g = res[key]["g"]
|
||||
ncuts = len(res[key]["cuts"])
|
||||
lines.append(
|
||||
f" tread {d}: |A(T)|={g.n} word={g.tooth_word} "
|
||||
f"bites={sorted(g.bites)} entry=e{res[d]['entry_edge']} "
|
||||
f"start_depth={res[d]['start_depth']} cuts={ncuts}")
|
||||
f" tread {d}.{comp}: |A(T)|={g.n} word={g.tooth_word} "
|
||||
f"bites={sorted(g.bites)} entry=e{res[key]['entry_edge']} "
|
||||
f"start_depth={res[key]['start_depth']} cuts={ncuts}")
|
||||
lines.append(
|
||||
f"final cut graph: {H.number_of_nodes()} vertices, "
|
||||
f"{H.number_of_edges()} edges, "
|
||||
@@ -393,16 +595,20 @@ def main() -> None:
|
||||
parser.add_argument("--seed", type=int, default=0)
|
||||
parser.add_argument("--flips", type=int, default=400,
|
||||
help="number of random diagonal flips when building G")
|
||||
parser.add_argument("--min-degree", type=int, default=5,
|
||||
help="reject random graphs below this minimum degree")
|
||||
parser.add_argument("--min-connectivity", type=int, default=5,
|
||||
help="reject random graphs below this node connectivity")
|
||||
parser.add_argument("--min-degree", type=int, default=None,
|
||||
help="compatibility alias; also raises min-connectivity")
|
||||
parser.add_argument("--attempts", type=int, default=1000,
|
||||
help="number of consecutive seeds to try for --min-degree")
|
||||
help="number of consecutive seeds to try for sampling")
|
||||
parser.add_argument("--json", metavar="PATH",
|
||||
help="write the full result as JSON to PATH")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = run_experiment(n=args.n, seed=args.seed, flips=args.flips,
|
||||
min_degree=args.min_degree, attempts=args.attempts)
|
||||
min_connectivity=args.min_connectivity,
|
||||
min_degree=args.min_degree,
|
||||
attempts=args.attempts)
|
||||
print(summary(result))
|
||||
if args.json:
|
||||
with open(args.json, "w") as fh:
|
||||
|
||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 140 KiB |
@@ -0,0 +1 @@
|
||||
"""Reusable medial tire cut helpers."""
|
||||
@@ -0,0 +1,431 @@
|
||||
"""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,
|
||||
shared_apexes: set[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
|
||||
}
|
||||
shared_apexes = shared_apexes or set()
|
||||
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 and apex not in shared_apexes
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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()
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Random medial tire decomposition 1
|
||||
|
||||
- original vertices: 30
|
||||
- original edges: 84
|
||||
- original node connectivity: 5
|
||||
- augmented vertices: 33
|
||||
- augmented edges: 93
|
||||
- same-level faces filled: 3
|
||||
- source vertex: 14
|
||||
- tire-tree nodes: 4
|
||||
- tire-tree edges: 3
|
||||
|
||||
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| T0 | 2 | 23 | 1 | 23 | 10 | 13 | 0 |
|
||||
| T1 | 1 | 15 | 1 | 15 | 5 | 10 | 0 |
|
||||
| T2 | 3 | 14 | 4 | 14 | 11 | 0 | 0 |
|
||||
| T3 | 3 | 5 | 1 | 5 | 5 | 0 | 0 |
|
||||
|
After Width: | Height: | Size: 341 KiB |
@@ -0,0 +1,16 @@
|
||||
"""Compatibility wrapper for the medial tire decomposition drawing script."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PAPER_DIR not in sys.path:
|
||||
sys.path.insert(0, PAPER_DIR)
|
||||
|
||||
from lib.draw_random_medial_tire_decompositions import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,311 +1,16 @@
|
||||
"""Exhaustive generator for full medial tire graphs, indexed by |A(T)|.
|
||||
|
||||
Model (Definitions/Remarks 3.1--3.9 of the medial tire decompositions paper).
|
||||
|
||||
* The annular medial vertices induce a cycle A(T), the *annular cycle*
|
||||
(Theorem 3.3). Write n = |A(T)| for its number of vertices = number of
|
||||
annular faces = number of annular edges e_0,...,e_{n-1}.
|
||||
|
||||
* Each edge e_i of A(T) carries exactly one tooth (a triangle of M(T))
|
||||
whose third vertex is a non-annular apex (Definition 3.4). A tooth is an
|
||||
*up tooth* (apex in the outer region) or a *down tooth* (apex in the inner
|
||||
region). We record the tooth types as a word in {U, D}^n.
|
||||
|
||||
* No two up teeth share an apex; at most two down teeth share an apex
|
||||
(Remark 3.5). Two down teeth sharing an apex form a *bite* (Definition
|
||||
3.7). So the down teeth are partitioned into singletons and bite pairs.
|
||||
A bite pairs two down-edges and is drawn as an apex inside the disk with
|
||||
spokes to the four endpoints; bites must be mutually non-crossing, i.e.
|
||||
the bite pairs form a non-crossing (laminar) matching of the down-edges.
|
||||
The two annular edges of a bite must be non-incident (Definition 3.7):
|
||||
they share no annular vertex, so cyclically adjacent edges cannot pair.
|
||||
|
||||
* There are at least three up teeth (Remark 3.6).
|
||||
|
||||
* Bite-face condition (Remark 3.8). Let B(T) = A(T) together with the bite
|
||||
apexes. Its interior non-tooth faces are the root face plus one inner-gap
|
||||
face per bite. A singleton down tooth lies in the innermost bite enclosing
|
||||
its edge (or in the root face if none). For every interior non-tooth face
|
||||
the number of down-tooth apexes lying in that face must be 0 or at least 3.
|
||||
Equivalently: no face holds exactly one or two singleton down teeth.
|
||||
|
||||
The generator enumerates, for a given n, every (tooth word, bite matching)
|
||||
pair satisfying these properties and emits the resulting full medial tire
|
||||
graph as an explicit vertex/edge structure. Configurations may optionally be
|
||||
reduced modulo the dihedral symmetry of the cycle.
|
||||
"""
|
||||
"""Compatibility wrapper for the medial tire generator now kept in ../lib."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Iterator
|
||||
import os
|
||||
import sys
|
||||
|
||||
# A bite is an unordered pair of down-edge indices (i, j) with i < j.
|
||||
Bite = tuple[int, int]
|
||||
Matching = frozenset[Bite]
|
||||
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PAPER_DIR not in sys.path:
|
||||
sys.path.insert(0, PAPER_DIR)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-crossing (laminar) matchings of the down edges.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def noncrossing_matchings(positions: tuple[int, ...]) -> tuple[Matching, ...]:
|
||||
"""All non-crossing partial matchings of ``positions`` (sorted ascending).
|
||||
|
||||
Bite pairs drawn inside the disk are non-crossing iff, read in cyclic
|
||||
order, no two pairs interleave. Cutting the cycle at the gap before the
|
||||
first edge turns this into ordinary non-crossing interval matchings, which
|
||||
obey the Catalan recursion below.
|
||||
"""
|
||||
if not positions:
|
||||
return (frozenset(),)
|
||||
head, *rest = positions
|
||||
out: list[Matching] = []
|
||||
# head left unmatched (a singleton down tooth, if its edge is down)
|
||||
for tail in noncrossing_matchings(tuple(rest)):
|
||||
out.append(tail)
|
||||
# head matched with positions[k]; the strictly-enclosed block must be
|
||||
# matched within itself to stay non-crossing.
|
||||
for k in range(1, len(positions)):
|
||||
partner = positions[k]
|
||||
inside = tuple(positions[1:k])
|
||||
outside = tuple(positions[k + 1:])
|
||||
for m_in in noncrossing_matchings(inside):
|
||||
for m_out in noncrossing_matchings(outside):
|
||||
out.append(frozenset({(head, partner)}) | m_in | m_out)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The bite-face condition (Remark 3.8).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def incident_edges(i: int, j: int, n: int) -> bool:
|
||||
"""Whether annular edges i and j share an annular vertex on the n-cycle."""
|
||||
return (j - i) % n == 1 or (i - j) % n == 1
|
||||
|
||||
|
||||
def has_incident_bite(bites: Matching, n: int) -> bool:
|
||||
"""Whether any bite pairs two incident (cyclically adjacent) edges."""
|
||||
return any(incident_edges(i, j, n) for i, j in bites)
|
||||
|
||||
|
||||
def innermost_bite(edge: int, bites: Matching) -> Bite | None:
|
||||
"""The minimal-span bite whose open interval contains ``edge``, or None."""
|
||||
enclosing = [b for b in bites if b[0] < edge < b[1]]
|
||||
if not enclosing:
|
||||
return None
|
||||
return min(enclosing, key=lambda b: b[1] - b[0])
|
||||
|
||||
|
||||
def face_singleton_counts(
|
||||
tooth_word: str, bites: Matching
|
||||
) -> dict[Bite | None, int]:
|
||||
"""Down-singletons per interior non-tooth face of B(T).
|
||||
|
||||
The key ``None`` is the root face; a bite key is that bite's inner-gap
|
||||
face. Faces with no singletons are simply absent from the result.
|
||||
"""
|
||||
matched = {edge for pair in bites for edge in pair}
|
||||
counts: dict[Bite | None, int] = defaultdict(int)
|
||||
for edge, tooth in enumerate(tooth_word):
|
||||
if tooth != "D" or edge in matched:
|
||||
continue # only singleton down teeth contribute apexes
|
||||
counts[innermost_bite(edge, bites)] += 1
|
||||
return dict(counts)
|
||||
|
||||
|
||||
def satisfies_bite_face_condition(tooth_word: str, bites: Matching) -> bool:
|
||||
"""Remark 3.8: every non-tooth face holds 0 or >=3 down-tooth apexes."""
|
||||
return all(count >= 3 for count in face_singleton_counts(tooth_word, bites).values())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The full medial tire graph as an explicit object.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FullMedialTireGraph:
|
||||
"""A full medial tire graph M(T) determined by its combinatorial data.
|
||||
|
||||
Vertices are named:
|
||||
a{k} annular medial vertex k (k = 0..n-1), forming A(T);
|
||||
u{i} apex of the up tooth on edge i;
|
||||
d{i} apex of the singleton down tooth on edge i;
|
||||
p{i}_{j} apex of the bite pairing edges i and j (i < j).
|
||||
"""
|
||||
|
||||
n: int
|
||||
tooth_word: str
|
||||
bites: Matching
|
||||
|
||||
@property
|
||||
def up_edges(self) -> tuple[int, ...]:
|
||||
return tuple(i for i, t in enumerate(self.tooth_word) if t == "U")
|
||||
|
||||
@property
|
||||
def down_edges(self) -> tuple[int, ...]:
|
||||
return tuple(i for i, t in enumerate(self.tooth_word) if t == "D")
|
||||
|
||||
@property
|
||||
def bite_edges(self) -> frozenset[int]:
|
||||
return frozenset(edge for pair in self.bites for edge in pair)
|
||||
|
||||
@property
|
||||
def singleton_down_edges(self) -> tuple[int, ...]:
|
||||
bite = self.bite_edges
|
||||
return tuple(i for i in self.down_edges if i not in bite)
|
||||
|
||||
def apex_of_edge(self, edge: int) -> str:
|
||||
if self.tooth_word[edge] == "U":
|
||||
return f"u{edge}"
|
||||
for i, j in self.bites:
|
||||
if edge in (i, j):
|
||||
return f"p{i}_{j}"
|
||||
return f"d{edge}"
|
||||
|
||||
def vertices(self) -> list[str]:
|
||||
verts = [f"a{k}" for k in range(self.n)]
|
||||
for i in self.up_edges:
|
||||
verts.append(f"u{i}")
|
||||
for i in self.singleton_down_edges:
|
||||
verts.append(f"d{i}")
|
||||
for i, j in sorted(self.bites):
|
||||
verts.append(f"p{i}_{j}")
|
||||
return verts
|
||||
|
||||
def edges(self) -> list[tuple[str, str]]:
|
||||
n = self.n
|
||||
out: list[tuple[str, str]] = []
|
||||
# annular cycle A(T)
|
||||
for k in range(n):
|
||||
out.append((f"a{k}", f"a{(k + 1) % n}"))
|
||||
# singleton teeth (up and down): two spokes each
|
||||
for i in self.up_edges:
|
||||
out += [(f"u{i}", f"a{i}"), (f"u{i}", f"a{(i + 1) % n}")]
|
||||
for i in self.singleton_down_edges:
|
||||
out += [(f"d{i}", f"a{i}"), (f"d{i}", f"a{(i + 1) % n}")]
|
||||
# bites: a shared apex with four spokes
|
||||
for i, j in sorted(self.bites):
|
||||
apex = f"p{i}_{j}"
|
||||
for edge in (i, j):
|
||||
out += [(apex, f"a{edge}"), (apex, f"a{(edge + 1) % n}")]
|
||||
return [tuple(sorted(e)) for e in out]
|
||||
|
||||
def canonical_key(self) -> tuple:
|
||||
"""Representative under the dihedral group of the cycle (rotations and
|
||||
reflections), so symmetric configurations collapse to one key."""
|
||||
n = self.n
|
||||
best: tuple | None = None
|
||||
for a in (1, -1):
|
||||
for b in range(n):
|
||||
relabel = lambda i: (a * i + b) % n
|
||||
word = [""] * n
|
||||
for i, t in enumerate(self.tooth_word):
|
||||
word[relabel(i)] = t
|
||||
mapped = tuple(sorted(
|
||||
tuple(sorted((relabel(i), relabel(j)))) for i, j in self.bites
|
||||
))
|
||||
key = (tuple(word), mapped)
|
||||
if best is None or key < best:
|
||||
best = key
|
||||
return best
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enumeration.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate(
|
||||
n: int, min_up_teeth: int = 3, dedup: bool = False
|
||||
) -> Iterator[FullMedialTireGraph]:
|
||||
"""Yield every full medial tire graph whose annular cycle has size ``n``.
|
||||
|
||||
``min_up_teeth`` defaults to 3 (Remark 3.6). With ``dedup`` set, only one
|
||||
representative per dihedral symmetry class is returned.
|
||||
"""
|
||||
seen: set[tuple] = set()
|
||||
for word_tuple in itertools.product("UD", repeat=n):
|
||||
tooth_word = "".join(word_tuple)
|
||||
if tooth_word.count("U") < min_up_teeth:
|
||||
continue
|
||||
down = tuple(i for i, t in enumerate(tooth_word) if t == "D")
|
||||
for bites in noncrossing_matchings(down):
|
||||
if has_incident_bite(bites, n):
|
||||
continue
|
||||
if not satisfies_bite_face_condition(tooth_word, bites):
|
||||
continue
|
||||
graph = FullMedialTireGraph(n=n, tooth_word=tooth_word, bites=bites)
|
||||
if dedup:
|
||||
key = graph.canonical_key()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
yield graph
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def figure_one() -> FullMedialTireGraph:
|
||||
"""The example graph of Figure 1 (Remark 3.8): 12 edges, one bite (0,6)."""
|
||||
return FullMedialTireGraph(
|
||||
n=12,
|
||||
tooth_word="DDDDDUDUUUUU", # edges 0-4,6 down; 5,7,8,9,10,11 up
|
||||
bites=frozenset({(0, 6)}),
|
||||
)
|
||||
|
||||
|
||||
def describe(graph: FullMedialTireGraph) -> str:
|
||||
counts = face_singleton_counts(graph.tooth_word, graph.bites)
|
||||
face_strs = []
|
||||
for face, c in sorted(counts.items(), key=lambda kv: (kv[0] is not None, kv[0])):
|
||||
name = "root" if face is None else f"bite{face}"
|
||||
face_strs.append(f"{name}:{c}")
|
||||
bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-"
|
||||
faces = " ".join(face_strs) or "-"
|
||||
return (
|
||||
f"word={graph.tooth_word} up={len(graph.up_edges)} "
|
||||
f"down={len(graph.down_edges)} bites={bites} faces[{faces}]"
|
||||
)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
if args.check_figure:
|
||||
g = figure_one()
|
||||
print("Figure 1 check:")
|
||||
print(f" {describe(g)}")
|
||||
ok = satisfies_bite_face_condition(g.tooth_word, g.bites)
|
||||
print(f" satisfies Remark 3.8: {ok} (expect True; faces 4 and 0)")
|
||||
print()
|
||||
|
||||
for n in range(args.min_n, args.max_n + 1):
|
||||
graphs = list(generate(n, min_up_teeth=args.min_up, dedup=args.dedup))
|
||||
label = "classes" if args.dedup else "graphs"
|
||||
print(f"n={n}: {len(graphs)} {label}")
|
||||
if args.show:
|
||||
for g in graphs[: args.show]:
|
||||
print(f" {describe(g)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--min-n", type=int, default=3)
|
||||
parser.add_argument("--max-n", type=int, default=8)
|
||||
parser.add_argument("--min-up", type=int, default=3, help="Remark 3.6 bound")
|
||||
parser.add_argument("--dedup", action="store_true",
|
||||
help="reduce modulo dihedral symmetry of the cycle")
|
||||
parser.add_argument("--show", type=int, default=0,
|
||||
help="print up to this many graphs per n")
|
||||
parser.add_argument("--check-figure", action="store_true",
|
||||
help="verify the Figure 1 example against Remark 3.8")
|
||||
run(parser.parse_args())
|
||||
from lib.full_medial_tire_generator import * # noqa: F401,F403
|
||||
from lib.full_medial_tire_generator import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Random medial tire decomposition 1
|
||||
|
||||
- original vertices: 30
|
||||
- original edges: 84
|
||||
- original node connectivity: 5
|
||||
- augmented vertices: 31
|
||||
- augmented edges: 87
|
||||
- same-level faces filled: 1
|
||||
- source vertex: 9
|
||||
- tire-tree nodes: 3
|
||||
- tire-tree edges: 2
|
||||
|
||||
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| T0 | 1 | 16 | 1 | 16 | 6 | 10 | 0 |
|
||||
| T1 | 2 | 20 | 1 | 20 | 10 | 10 | 0 |
|
||||
| T2 | 3 | 16 | 3 | 16 | 12 | 0 | 1 |
|
||||
|
After Width: | Height: | Size: 294 KiB |
@@ -0,0 +1,18 @@
|
||||
# Random medial tire decomposition 2
|
||||
|
||||
- original vertices: 30
|
||||
- original edges: 84
|
||||
- original node connectivity: 5
|
||||
- augmented vertices: 32
|
||||
- augmented edges: 90
|
||||
- same-level faces filled: 2
|
||||
- source vertex: 4
|
||||
- tire-tree nodes: 4
|
||||
- tire-tree edges: 3
|
||||
|
||||
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| T0 | 1 | 16 | 1 | 16 | 7 | 9 | 0 |
|
||||
| T1 | 2 | 17 | 1 | 17 | 9 | 8 | 0 |
|
||||
| T2 | 3 | 14 | 1 | 14 | 8 | 4 | 1 |
|
||||
| T3 | 4 | 6 | 2 | 6 | 5 | 0 | 0 |
|
||||
|
After Width: | Height: | Size: 328 KiB |
@@ -19,14 +19,25 @@ is a bite.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import random
|
||||
from collections import defaultdict
|
||||
import sys
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import scipy.spatial
|
||||
|
||||
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PAPER_DIR not in sys.path:
|
||||
sys.path.insert(0, PAPER_DIR)
|
||||
|
||||
from lib.medial_tire_decomposition import (
|
||||
ekey,
|
||||
extract_tread,
|
||||
medial_tire_facemodel,
|
||||
recognise,
|
||||
)
|
||||
|
||||
|
||||
def random_sphere_triangulation(n: int, seed: int) -> nx.Graph:
|
||||
"""A random maximal planar graph: convex hull of random points on S^2."""
|
||||
@@ -40,19 +51,6 @@ def random_sphere_triangulation(n: int, seed: int) -> nx.Graph:
|
||||
return g
|
||||
|
||||
|
||||
def medial_tire_facemodel(tread_faces) -> nx.Graph:
|
||||
"""Full medial tire graph M(T): the ambient tread-face model. Each tread
|
||||
face contributes a triangle on its three edge-medial-vertices; no medial
|
||||
edges between two same-boundary edges (those come from non-tread corners)."""
|
||||
Mt = nx.Graph()
|
||||
for f in tread_faces:
|
||||
es = [ekey(f[0], f[1]), ekey(f[1], f[2]), ekey(f[2], f[0])]
|
||||
Mt.add_nodes_from(es)
|
||||
for a in range(3):
|
||||
Mt.add_edge(es[a], es[(a + 1) % 3])
|
||||
return Mt
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Maximal planar graph on n vertices: stacked seed + random diagonal flips.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -89,10 +87,6 @@ def random_maximal_planar(n: int, seed: int, flips: int = 400) -> nx.Graph:
|
||||
return g
|
||||
|
||||
|
||||
def ekey(u, v):
|
||||
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
|
||||
|
||||
|
||||
def triangular_faces(g: nx.Graph):
|
||||
ok, emb = nx.check_planarity(g)
|
||||
if not ok:
|
||||
@@ -146,147 +140,12 @@ def proper_3_colorings(M: nx.Graph, limit: int):
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Tread extraction from a BFS-level decomposition.
|
||||
# Classify colourings of recognised full medial tire graphs.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def extract_tread(faces, levels, d):
|
||||
"""Tread T_d: faces spanning levels {d, d+1}. Returns its edge classes."""
|
||||
tread_faces = []
|
||||
for f in faces:
|
||||
lv = [levels[x] for x in f]
|
||||
if min(lv) == d and max(lv) == d + 1:
|
||||
tread_faces.append(f)
|
||||
if not tread_faces:
|
||||
return None
|
||||
annular, up, down = set(), set(), set()
|
||||
face_of_down = defaultdict(int)
|
||||
for f in tread_faces:
|
||||
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
|
||||
e = ekey(x, y)
|
||||
lx, ly = levels[x], levels[y]
|
||||
if {lx, ly} == {d, d + 1}:
|
||||
annular.add(e)
|
||||
elif lx == ly == d:
|
||||
up.add(e)
|
||||
elif lx == ly == d + 1:
|
||||
down.add(e)
|
||||
face_of_down[e] += 1
|
||||
bites = {e for e in down if face_of_down[e] == 2}
|
||||
return {
|
||||
"tread_faces": tread_faces,
|
||||
"annular": annular, "up": up, "down": down, "bites": bites,
|
||||
}
|
||||
|
||||
|
||||
def annular_cycle_order(M: nx.Graph, annular: set):
|
||||
"""Cyclic order of the annular medial vertices (they induce a cycle)."""
|
||||
sub = M.subgraph(annular)
|
||||
if sub.number_of_nodes() == 0 or any(sub.degree(v) != 2 for v in sub):
|
||||
return None
|
||||
if not nx.is_connected(sub):
|
||||
return None
|
||||
start = next(iter(annular))
|
||||
order = [start]
|
||||
prev = None
|
||||
cur = start
|
||||
while True:
|
||||
nbrs = [w for w in sub.neighbors(cur) if w != prev]
|
||||
if not nbrs:
|
||||
break
|
||||
nxt = nbrs[0]
|
||||
if nxt == start:
|
||||
break
|
||||
order.append(nxt)
|
||||
prev, cur = cur, nxt
|
||||
return order if len(order) == len(annular) else None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Recognise a genuine tread as a FullMedialTireGraph and classify colourings.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
from full_medial_tire_generator import FullMedialTireGraph
|
||||
from kempe_valid_colorings import classify as kempe_classify
|
||||
|
||||
|
||||
def _linear_cut(n, bite_pairs):
|
||||
"""Rotate the cycle so the bite pairs become linear non-crossing intervals."""
|
||||
for r in range(n):
|
||||
rel = [tuple(sorted(((i - r) % n, (j - r) % n))) for i, j in bite_pairs]
|
||||
ok = True
|
||||
for a, b in rel:
|
||||
for c, d in rel:
|
||||
if (a, b) != (c, d) and (a < c < b < d or c < a < d < b):
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
break
|
||||
if ok:
|
||||
return r, rel
|
||||
return None
|
||||
|
||||
|
||||
def recognise(M, tread):
|
||||
"""Return (FullMedialTireGraph, bijection fmt-name -> medial vertex) or None.
|
||||
|
||||
``M`` here is the tread-face model M(T) (cycle + teeth + bites)."""
|
||||
annular = tread["annular"]
|
||||
order = annular_cycle_order(M, annular)
|
||||
if order is None or len(order) < 3:
|
||||
return None
|
||||
n = len(order)
|
||||
ann_set = set(annular)
|
||||
|
||||
apex_of_edge = []
|
||||
for i in range(n):
|
||||
a, b = order[i], order[(i + 1) % n]
|
||||
common = [w for w in set(M.neighbors(a)) & set(M.neighbors(b)) if w not in ann_set]
|
||||
if len(common) != 1:
|
||||
return None
|
||||
apex_of_edge.append(common[0])
|
||||
|
||||
up = set(tread["up"])
|
||||
# bite apex: serves two cycle edges (== adjacent to four annular vertices)
|
||||
apex_positions = defaultdict(list)
|
||||
for i, ap in enumerate(apex_of_edge):
|
||||
apex_positions[ap].append(i)
|
||||
|
||||
tooth = []
|
||||
bite_pairs = []
|
||||
for ap, positions in apex_positions.items():
|
||||
if len(positions) == 2:
|
||||
bite_pairs.append(tuple(sorted(positions)))
|
||||
for i, ap in enumerate(apex_of_edge):
|
||||
tooth.append("U" if ap in up else "D")
|
||||
|
||||
cut = _linear_cut(n, bite_pairs)
|
||||
if cut is None:
|
||||
return None
|
||||
r, rel_bites = cut
|
||||
word = [""] * n
|
||||
for i in range(n):
|
||||
word[(i - r) % n] = tooth[i]
|
||||
g = FullMedialTireGraph(n=n, tooth_word="".join(word), bites=frozenset(rel_bites))
|
||||
|
||||
# bijection fmt-name -> medial vertex
|
||||
bij = {}
|
||||
for k in range(n):
|
||||
bij[f"a{k}"] = order[(k + r) % n]
|
||||
for i in g.up_edges:
|
||||
bij[f"u{i}"] = apex_of_edge[(i + r) % n]
|
||||
for i in g.singleton_down_edges:
|
||||
bij[f"d{i}"] = apex_of_edge[(i + r) % n]
|
||||
for (i, j) in sorted(g.bites):
|
||||
bij[f"p{i}_{j}"] = apex_of_edge[(i + r) % n]
|
||||
|
||||
# verify the reconstructed graph is edge-faithful to the tread-face M(T)
|
||||
mt_edges = {ekey(*e) for e in M.edges()}
|
||||
rec_edges = {ekey(bij[u], bij[v]) for u, v in g.edges()}
|
||||
if rec_edges != mt_edges:
|
||||
return None
|
||||
return g, bij
|
||||
|
||||
|
||||
def canonical(coloring, ordered):
|
||||
remap, out = {}, []
|
||||
for v in ordered:
|
||||
@@ -341,32 +200,29 @@ def iter_pieces(seed: int, color_limit: int = 400000):
|
||||
if tread is None or len(tread["up"]) < 3:
|
||||
continue
|
||||
mt = medial_tire_facemodel(tread["tread_faces"])
|
||||
rec = recognise(mt, tread)
|
||||
if rec is None:
|
||||
continue
|
||||
g, bij = rec
|
||||
mt_nodes = list(bij.values())
|
||||
name_of = {v: k for k, v in bij.items()}
|
||||
for comp, (g, bij) in enumerate(recognise(mt, tread)):
|
||||
mt_nodes = list(bij.values())
|
||||
name_of = {v: k for k, v in bij.items()}
|
||||
|
||||
realized = set()
|
||||
for col in global_colorings:
|
||||
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
|
||||
realized = set()
|
||||
for col in global_colorings:
|
||||
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
|
||||
|
||||
colorings = []
|
||||
seen = set()
|
||||
for col in proper_3_colorings_subgraph(mt, mt_nodes):
|
||||
key = canonical(col, mt_nodes)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
fmt_col = {name_of[v]: c for v, c in col.items()}
|
||||
balanced = kempe_classify(g, fmt_col).valid
|
||||
is_real = key in realized
|
||||
cat = ("Invalid" if not balanced
|
||||
else "Realized" if is_real else "Unrealized")
|
||||
colorings.append((fmt_col, cat))
|
||||
meta = {"source": s, "tread": d}
|
||||
yield (meta, g, colorings)
|
||||
colorings = []
|
||||
seen = set()
|
||||
for col in proper_3_colorings_subgraph(mt, mt_nodes):
|
||||
key = canonical(col, mt_nodes)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
fmt_col = {name_of[v]: c for v, c in col.items()}
|
||||
balanced = kempe_classify(g, fmt_col).valid
|
||||
is_real = key in realized
|
||||
cat = ("Invalid" if not balanced
|
||||
else "Realized" if is_real else "Unrealized")
|
||||
colorings.append((fmt_col, cat))
|
||||
meta = {"source": s, "tread": d, "comp": comp}
|
||||
yield (meta, g, colorings)
|
||||
|
||||
|
||||
def analyse(seed: int, color_limit: int = 400000):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Reusable helpers for medial tire decomposition experiments."""
|
||||
@@ -0,0 +1,860 @@
|
||||
"""Draw medial tire decompositions of random 5-connected triangulations.
|
||||
|
||||
The source graphs come from ``plantri -c5`` in graph6 format. For each sampled
|
||||
30-vertex triangulation, this script chooses a random source vertex, builds the
|
||||
BFS depth-component tire tree, and draws both the tire tree and the medial
|
||||
tread model for each depth component.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
PAPER_DIR = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = PAPER_DIR.parents[1]
|
||||
os.environ.setdefault(
|
||||
"MPLCONFIGDIR", str(PAPER_DIR / "experiments" / ".matplotlib-cache")
|
||||
)
|
||||
os.environ.setdefault("XDG_CACHE_HOME", str(PAPER_DIR / "experiments" / ".cache"))
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.patches import PathPatch
|
||||
from matplotlib.path import Path as MplPath
|
||||
import networkx as nx
|
||||
|
||||
if str(PAPER_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(PAPER_DIR))
|
||||
|
||||
from lib.medial_tire_decomposition import (
|
||||
annular_cycle_components,
|
||||
ekey,
|
||||
medial_tire_facemodel,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TreadNode:
|
||||
idx: int
|
||||
depth: int
|
||||
face_indices: tuple[int, ...]
|
||||
annular: frozenset
|
||||
up: frozenset
|
||||
down: frozenset
|
||||
bites: frozenset
|
||||
medial: nx.Graph
|
||||
annular_cycles: tuple[tuple, ...]
|
||||
|
||||
|
||||
@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]:
|
||||
cmd = [str(REPO_ROOT / "plantri" / "plantri"), "-g", "-c5", str(n)]
|
||||
rng = random.Random(seed)
|
||||
sample: list[tuple[int, nx.Graph]] = []
|
||||
seen = 0
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
||||
assert proc.stdout is not None
|
||||
for raw in proc.stdout:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith(b">>"):
|
||||
continue
|
||||
graph = nx.from_graph6_bytes(line)
|
||||
if nx.node_connectivity(graph) < 5:
|
||||
continue
|
||||
seen += 1
|
||||
if len(sample) < count:
|
||||
sample.append((seen, graph))
|
||||
else:
|
||||
j = rng.randrange(seen)
|
||||
if j < count:
|
||||
sample[j] = (seen, graph)
|
||||
if seen >= scan_limit:
|
||||
proc.terminate()
|
||||
break
|
||||
proc.wait(timeout=10)
|
||||
if len(sample) < count:
|
||||
raise RuntimeError(f"only found {len(sample)} graphs after scanning {seen}")
|
||||
return [graph for _ordinal, graph in sample]
|
||||
|
||||
|
||||
def triangular_faces(g: nx.Graph):
|
||||
ok, emb = nx.check_planarity(g)
|
||||
if not ok:
|
||||
raise ValueError("not planar")
|
||||
seen = set()
|
||||
faces = []
|
||||
for u, v in list(emb.edges()):
|
||||
if (u, v) in seen:
|
||||
continue
|
||||
face = tuple(emb.traverse_face(u, v, mark_half_edges=seen))
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def edge_face_data(faces):
|
||||
face_edges = []
|
||||
edge_faces: dict[tuple, list[int]] = defaultdict(list)
|
||||
for i, face in enumerate(faces):
|
||||
edges = {
|
||||
ekey(face[0], face[1]),
|
||||
ekey(face[1], face[2]),
|
||||
ekey(face[2], face[0]),
|
||||
}
|
||||
face_edges.append(edges)
|
||||
for edge in edges:
|
||||
edge_faces[edge].append(i)
|
||||
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):
|
||||
depths = [min(levels[v] for v in face) for face in faces]
|
||||
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])
|
||||
|
||||
comps = []
|
||||
seen = [False] * len(faces)
|
||||
for start in range(len(faces)):
|
||||
if seen[start]:
|
||||
continue
|
||||
depth = depths[start]
|
||||
stack = [start]
|
||||
comp = []
|
||||
seen[start] = True
|
||||
while stack:
|
||||
face = stack.pop()
|
||||
comp.append(face)
|
||||
for other in dual_adj[face]:
|
||||
if not seen[other] and depths[other] == depth:
|
||||
seen[other] = True
|
||||
stack.append(other)
|
||||
comps.append((depth, tuple(sorted(comp))))
|
||||
return comps, depths, dual_adj
|
||||
|
||||
|
||||
def tread_from_component(faces, levels, face_indices):
|
||||
tread_faces = [faces[i] for i in face_indices]
|
||||
if not tread_faces:
|
||||
return None
|
||||
depth = min(min(levels[v] for v in face) for face in tread_faces)
|
||||
annular, up, down = set(), set(), set()
|
||||
face_of_down = defaultdict(int)
|
||||
for face in tread_faces:
|
||||
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
|
||||
e = ekey(x, y)
|
||||
lx, ly = levels[x], levels[y]
|
||||
if {lx, ly} == {depth, depth + 1}:
|
||||
annular.add(e)
|
||||
elif lx == ly == depth:
|
||||
up.add(e)
|
||||
elif lx == ly == depth + 1:
|
||||
down.add(e)
|
||||
face_of_down[e] += 1
|
||||
if len(annular) < 3:
|
||||
return None
|
||||
return {
|
||||
"tread_faces": tread_faces,
|
||||
"annular": annular,
|
||||
"up": up,
|
||||
"down": down,
|
||||
"bites": {e for e in down if face_of_down[e] == 2},
|
||||
}
|
||||
|
||||
|
||||
def build_tire_tree(g: nx.Graph, source: int, augment: bool = True):
|
||||
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)
|
||||
levels = nx.single_source_shortest_path_length(work_graph, source)
|
||||
comps, depths, dual_adj = depth_components(faces, face_edges, edge_faces, levels)
|
||||
comp_of_face = {}
|
||||
for comp_idx, (_depth, face_indices) in enumerate(comps):
|
||||
for face in face_indices:
|
||||
comp_of_face[face] = comp_idx
|
||||
|
||||
nodes: list[TreadNode] = []
|
||||
comp_to_node = {}
|
||||
for comp_idx, (depth, face_indices) in enumerate(comps):
|
||||
tread = tread_from_component(faces, levels, face_indices)
|
||||
if tread is None or len(tread["up"]) < 3:
|
||||
continue
|
||||
mt = medial_tire_facemodel(tread["tread_faces"])
|
||||
annular_cycles = tuple(annular_cycle_components(mt, tread["annular"]))
|
||||
if not annular_cycles:
|
||||
continue
|
||||
node = TreadNode(
|
||||
idx=len(nodes),
|
||||
depth=depth,
|
||||
face_indices=face_indices,
|
||||
annular=frozenset(tread["annular"]),
|
||||
up=frozenset(tread["up"]),
|
||||
down=frozenset(tread["down"]),
|
||||
bites=frozenset(tread["bites"]),
|
||||
medial=mt,
|
||||
annular_cycles=annular_cycles,
|
||||
)
|
||||
comp_to_node[comp_idx] = node.idx
|
||||
nodes.append(node)
|
||||
|
||||
tree_edges = set()
|
||||
for comp_idx, (depth, face_indices) in enumerate(comps):
|
||||
if comp_idx not in comp_to_node:
|
||||
continue
|
||||
child = comp_to_node[comp_idx]
|
||||
parent_candidates = set()
|
||||
for face in face_indices:
|
||||
for other in dual_adj[face]:
|
||||
other_comp = comp_of_face[other]
|
||||
if depths[other] == depth - 1 and other_comp in comp_to_node:
|
||||
parent_candidates.add(comp_to_node[other_comp])
|
||||
for parent in parent_candidates:
|
||||
tree_edges.add((parent, child))
|
||||
return augmentation, faces, levels, nodes, sorted(tree_edges)
|
||||
|
||||
|
||||
def graph_layout(g: nx.Graph):
|
||||
try:
|
||||
return nx.planar_layout(g)
|
||||
except nx.NetworkXException:
|
||||
return nx.spring_layout(g, seed=0)
|
||||
|
||||
|
||||
def draw_base_graph(ax, g, levels, source, added_vertices=()):
|
||||
pos = graph_layout(g)
|
||||
max_level = max(levels.values())
|
||||
cmap = plt.get_cmap("viridis", max_level + 1)
|
||||
node_colors = [cmap(levels[v]) for v in g.nodes()]
|
||||
nx.draw_networkx_edges(g, pos, ax=ax, edge_color="#cbd5e1", width=0.8)
|
||||
added_set = set(added_vertices)
|
||||
nx.draw_networkx_nodes(
|
||||
g,
|
||||
pos,
|
||||
ax=ax,
|
||||
node_color=node_colors,
|
||||
node_size=[
|
||||
150 if v == source else 96 if v in added_set else 72
|
||||
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()}
|
||||
nx.draw_networkx_labels(g, pos, labels=labels, ax=ax, font_size=5)
|
||||
ax.set_title(
|
||||
f"Augmented G, source {source}; vertex levels 0..{max_level}",
|
||||
fontsize=10,
|
||||
)
|
||||
ax.set_aspect("equal")
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
def tree_positions(nodes: list[TreadNode], tree_edges):
|
||||
children: dict[int, list[int]] = defaultdict(list)
|
||||
has_parent = set()
|
||||
for parent, child in tree_edges:
|
||||
children[parent].append(child)
|
||||
has_parent.add(child)
|
||||
roots = [node.idx for node in nodes if node.idx not in has_parent]
|
||||
for child_list in children.values():
|
||||
child_list.sort(key=lambda idx: (nodes[idx].depth, idx))
|
||||
|
||||
x_counter = 0
|
||||
pos = {}
|
||||
|
||||
def place(idx, depth):
|
||||
nonlocal x_counter
|
||||
if not children[idx]:
|
||||
pos[idx] = (x_counter, -depth)
|
||||
x_counter += 1
|
||||
return pos[idx][0]
|
||||
xs = [place(child, depth + 1) for child in children[idx]]
|
||||
x = sum(xs) / len(xs)
|
||||
pos[idx] = (x, -depth)
|
||||
return x
|
||||
|
||||
for root in sorted(roots, key=lambda idx: (nodes[idx].depth, idx)):
|
||||
place(root, 0)
|
||||
x_counter += 1
|
||||
return pos
|
||||
|
||||
|
||||
def draw_tire_tree(ax, nodes: list[TreadNode], tree_edges):
|
||||
pos = tree_positions(nodes, tree_edges)
|
||||
for parent, child in tree_edges:
|
||||
x0, y0 = pos[parent]
|
||||
x1, y1 = pos[child]
|
||||
ax.plot([x0, x1], [y0, y1], color="#374151", lw=1.0, zorder=1)
|
||||
for node in nodes:
|
||||
x, y = pos[node.idx]
|
||||
ax.text(
|
||||
x,
|
||||
y,
|
||||
f"T{node.idx}\nd={node.depth}\n{len(node.annular_cycles)} cycle(s)",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=8,
|
||||
bbox={
|
||||
"boxstyle": "round,pad=0.32",
|
||||
"facecolor": "#fef3c7",
|
||||
"edgecolor": "#111827",
|
||||
"linewidth": 0.9,
|
||||
},
|
||||
zorder=3,
|
||||
)
|
||||
ax.set_title("Depth-component tire tree", fontsize=10)
|
||||
if pos:
|
||||
xs = [p[0] for p in pos.values()]
|
||||
ys = [p[1] for p in pos.values()]
|
||||
ax.set_xlim(min(xs) - 1.0, max(xs) + 1.0)
|
||||
ax.set_ylim(min(ys) - 0.7, max(ys) + 0.7)
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
def vertex_xy(k: int, n: int, radius: float, rotation: float = 0.0) -> tuple[float, float]:
|
||||
angle = math.pi / 2 + rotation - 2 * math.pi * k / n
|
||||
return radius * math.cos(angle), radius * math.sin(angle)
|
||||
|
||||
|
||||
def edge_midpoint_angle(i: int, n: int, rotation: float = 0.0) -> float:
|
||||
return math.pi / 2 + rotation - 2 * math.pi * (i + 0.5) / n
|
||||
|
||||
|
||||
def annular_cycle_edges(node: TreadNode) -> set[tuple]:
|
||||
edges = set()
|
||||
for order in node.annular_cycles:
|
||||
for i, a in enumerate(order):
|
||||
b = order[(i + 1) % len(order)]
|
||||
edges.add(tuple(sorted((a, b))))
|
||||
return edges
|
||||
|
||||
|
||||
def shared_up_apex_occurrences(node: TreadNode) -> dict[tuple, list[tuple[int, int]]]:
|
||||
occurrences: dict[tuple, list[tuple[int, int]]] = defaultdict(list)
|
||||
annular = set(node.annular)
|
||||
for cycle_idx, order in enumerate(node.annular_cycles):
|
||||
n = len(order)
|
||||
for i, a in enumerate(order):
|
||||
b = order[(i + 1) % n]
|
||||
apexes = [
|
||||
w for w in set(node.medial.neighbors(a)) & set(node.medial.neighbors(b))
|
||||
if w not in annular
|
||||
]
|
||||
for apex in apexes:
|
||||
if apex in node.up:
|
||||
occurrences[apex].append((cycle_idx, i))
|
||||
return {apex: where for apex, where in occurrences.items() if len(where) > 1}
|
||||
|
||||
|
||||
def compound_cycle_rotations(node: TreadNode) -> list[float]:
|
||||
rotations = [0.0] * len(node.annular_cycles)
|
||||
shared = shared_up_apex_occurrences(node)
|
||||
by_cycle: dict[int, list[int]] = defaultdict(list)
|
||||
for occurrences in shared.values():
|
||||
for cycle_idx, edge_idx in occurrences:
|
||||
by_cycle[cycle_idx].append(edge_idx)
|
||||
|
||||
for cycle_idx, edge_indices in by_cycle.items():
|
||||
n = len(node.annular_cycles[cycle_idx])
|
||||
sx = sy = 0.0
|
||||
for edge_idx in edge_indices:
|
||||
angle = edge_midpoint_angle(edge_idx, n)
|
||||
sx += math.cos(angle)
|
||||
sy += math.sin(angle)
|
||||
if sx or sy:
|
||||
rotations[cycle_idx] = math.pi / 2 - math.atan2(sy, sx)
|
||||
return rotations
|
||||
|
||||
|
||||
def compound_cycle_order(node: TreadNode) -> list[int]:
|
||||
shared = shared_up_apex_occurrences(node)
|
||||
adj: dict[int, set[int]] = defaultdict(set)
|
||||
for occurrences in shared.values():
|
||||
cycles = sorted({cycle_idx for cycle_idx, _edge_idx in occurrences})
|
||||
for a, b in zip(cycles, cycles[1:]):
|
||||
adj[a].add(b)
|
||||
adj[b].add(a)
|
||||
|
||||
remaining = set(range(len(node.annular_cycles)))
|
||||
order = []
|
||||
while remaining:
|
||||
candidates = sorted(
|
||||
remaining,
|
||||
key=lambda idx: (len(adj[idx]) if adj[idx] else 999, idx),
|
||||
)
|
||||
start = candidates[0]
|
||||
stack = [(start, None)]
|
||||
while stack:
|
||||
current, parent = stack.pop()
|
||||
if current not in remaining:
|
||||
continue
|
||||
remaining.remove(current)
|
||||
order.append(current)
|
||||
children = sorted(
|
||||
(neighbor for neighbor in adj[current] if neighbor != parent),
|
||||
key=lambda idx: (len(adj[idx]), idx),
|
||||
reverse=True,
|
||||
)
|
||||
for child in children:
|
||||
stack.append((child, current))
|
||||
return order
|
||||
|
||||
|
||||
def cycle_layout(
|
||||
node: TreadNode,
|
||||
rotations: list[float],
|
||||
display_order: list[int] | None = None,
|
||||
cycle_spacing: float = 3.25,
|
||||
):
|
||||
cycle_count = len(node.annular_cycles)
|
||||
if display_order is None:
|
||||
display_order = list(range(cycle_count))
|
||||
rank = {cycle_idx: i for i, cycle_idx in enumerate(display_order)}
|
||||
offsets = [cycle_spacing * (rank[i] - (cycle_count - 1) / 2) for i in range(cycle_count)]
|
||||
ann_positions: dict[tuple[int, tuple], tuple[float, float]] = {}
|
||||
apex_positions: dict[tuple[int, tuple], tuple[float, float]] = {}
|
||||
apex_corners: dict[tuple[int, tuple], list[tuple[float, float]]] = defaultdict(list)
|
||||
|
||||
for cycle_idx, order in enumerate(node.annular_cycles):
|
||||
n = len(order)
|
||||
dx = offsets[cycle_idx]
|
||||
rotation = rotations[cycle_idx]
|
||||
for k, vertex in enumerate(order):
|
||||
x, y = vertex_xy(k, n, 1.0, rotation)
|
||||
ann_positions[(cycle_idx, vertex)] = (dx + x, y)
|
||||
|
||||
for i, a in enumerate(order):
|
||||
b = order[(i + 1) % n]
|
||||
apexes = [
|
||||
w for w in set(node.medial.neighbors(a)) & set(node.medial.neighbors(b))
|
||||
if w not in node.annular
|
||||
]
|
||||
for apex in apexes:
|
||||
key = (cycle_idx, apex)
|
||||
apex_corners[key].extend([
|
||||
ann_positions[(cycle_idx, a)],
|
||||
ann_positions[(cycle_idx, b)],
|
||||
])
|
||||
if key in apex_positions:
|
||||
continue
|
||||
angle = edge_midpoint_angle(i, n, rotation)
|
||||
radius = 1.42 if apex in node.up else 0.58
|
||||
apex_positions[key] = (
|
||||
dx + radius * math.cos(angle),
|
||||
radius * math.sin(angle),
|
||||
)
|
||||
|
||||
for key, corners in apex_corners.items():
|
||||
_cycle_idx, apex = key
|
||||
if apex in node.bites and corners:
|
||||
cx = sum(p[0] for p in corners) / len(corners)
|
||||
cy = sum(p[1] for p in corners) / len(corners)
|
||||
apex_positions[key] = (0.82 * cx, 0.82 * cy)
|
||||
|
||||
return offsets, ann_positions, apex_positions, apex_corners
|
||||
|
||||
|
||||
def orientation(a, b, c) -> float:
|
||||
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
||||
|
||||
|
||||
def segments_cross(a, b, c, d) -> bool:
|
||||
eps = 1e-9
|
||||
if max(a[0], b[0]) + eps < min(c[0], d[0]):
|
||||
return False
|
||||
if max(c[0], d[0]) + eps < min(a[0], b[0]):
|
||||
return False
|
||||
if max(a[1], b[1]) + eps < min(c[1], d[1]):
|
||||
return False
|
||||
if max(c[1], d[1]) + eps < min(a[1], b[1]):
|
||||
return False
|
||||
o1 = orientation(a, b, c)
|
||||
o2 = orientation(a, b, d)
|
||||
o3 = orientation(c, d, a)
|
||||
o4 = orientation(c, d, b)
|
||||
return (o1 * o2 < -eps) and (o3 * o4 < -eps)
|
||||
|
||||
|
||||
def polylines_cross(first, second) -> bool:
|
||||
for i in range(len(first) - 1):
|
||||
for j in range(len(second) - 1):
|
||||
if segments_cross(first[i], first[i + 1], second[j], second[j + 1]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def quadratic_points(start, control, end, samples: int = 40):
|
||||
points = []
|
||||
for k in range(samples + 1):
|
||||
t = k / samples
|
||||
x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control[0] + t ** 2 * end[0]
|
||||
y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control[1] + t ** 2 * end[1]
|
||||
points.append((x, y))
|
||||
return points
|
||||
|
||||
|
||||
def crossing_free_shared_arcs(
|
||||
node: TreadNode,
|
||||
apex_positions,
|
||||
top_y: float,
|
||||
bottom_y: float,
|
||||
):
|
||||
shared = shared_up_apex_occurrences(node)
|
||||
arc_specs = []
|
||||
routed = []
|
||||
pairs = []
|
||||
for apex, occurrences in shared.items():
|
||||
ordered = sorted(occurrences, key=lambda item: apex_positions[(item[0], apex)][0])
|
||||
for left, right in zip(ordered, ordered[1:]):
|
||||
pairs.append((apex, left, right))
|
||||
|
||||
pairs.sort(key=lambda item: (
|
||||
abs(apex_positions[(item[1][0], item[0])][0] - apex_positions[(item[2][0], item[0])][0]),
|
||||
min(item[1][0], item[2][0]),
|
||||
item[0],
|
||||
))
|
||||
|
||||
for arc_idx, (apex, left, right) in enumerate(pairs):
|
||||
start = apex_positions[(left[0], apex)]
|
||||
end = apex_positions[(right[0], apex)]
|
||||
if start[0] > end[0]:
|
||||
start, end = end, start
|
||||
for lane in range(64):
|
||||
candidates = [
|
||||
((start[0] + end[0]) / 2, top_y + 0.42 + 0.18 * lane),
|
||||
((start[0] + end[0]) / 2, bottom_y - 0.42 - 0.18 * lane),
|
||||
]
|
||||
for control in candidates:
|
||||
points = quadratic_points(start, control, end)
|
||||
if not any(polylines_cross(points, existing) for existing in routed):
|
||||
routed.append(points)
|
||||
arc_specs.append((start, control, end))
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
control = ((start[0] + end[0]) / 2, top_y + 0.42 + 0.18 * (64 + arc_idx))
|
||||
routed.append(quadratic_points(start, control, end))
|
||||
arc_specs.append((start, control, end))
|
||||
return arc_specs
|
||||
|
||||
|
||||
def draw_tread_cycles(ax, node: TreadNode, connect_shared: bool):
|
||||
rotations = compound_cycle_rotations(node) if connect_shared else [0.0] * len(node.annular_cycles)
|
||||
display_order = compound_cycle_order(node) if connect_shared else None
|
||||
offsets, ann_positions, apex_positions, apex_corners = cycle_layout(
|
||||
node, rotations, display_order=display_order
|
||||
)
|
||||
|
||||
for cycle_idx, order in enumerate(node.annular_cycles):
|
||||
cyc_x = [ann_positions[(cycle_idx, v)][0] for v in order] + [
|
||||
ann_positions[(cycle_idx, order[0])][0]
|
||||
]
|
||||
cyc_y = [ann_positions[(cycle_idx, v)][1] for v in order] + [
|
||||
ann_positions[(cycle_idx, order[0])][1]
|
||||
]
|
||||
ax.plot(cyc_x, cyc_y, color="black", lw=1.3, zorder=2)
|
||||
|
||||
for key, corners in apex_corners.items():
|
||||
pos = apex_positions[key]
|
||||
for corner in corners:
|
||||
ax.plot([pos[0], corner[0]], [pos[1], corner[1]], color="#9ca3af", lw=0.5)
|
||||
|
||||
if connect_shared and apex_positions:
|
||||
all_positions = list(ann_positions.values()) + list(apex_positions.values())
|
||||
top_y = max(p[1] for p in all_positions)
|
||||
bottom_y = min(p[1] for p in all_positions)
|
||||
for start, control, end in crossing_free_shared_arcs(
|
||||
node, apex_positions, top_y, bottom_y
|
||||
):
|
||||
path = MplPath([start, control, end], [MplPath.MOVETO, MplPath.CURVE3, MplPath.CURVE3])
|
||||
ax.add_patch(
|
||||
PathPatch(
|
||||
path,
|
||||
facecolor="none",
|
||||
edgecolor="#475569",
|
||||
lw=0.8,
|
||||
linestyle=(0, (1.2, 2.0)),
|
||||
zorder=1,
|
||||
)
|
||||
)
|
||||
|
||||
for (_cycle_idx, apex), pos in apex_positions.items():
|
||||
if apex in node.up:
|
||||
color, size, edgecolor = "#2563eb", 13, "none"
|
||||
elif apex in node.bites:
|
||||
color, size, edgecolor = "#7f1d1d", 24, "black"
|
||||
else:
|
||||
color, size, edgecolor = "#dc2626", 13, "none"
|
||||
ax.scatter(
|
||||
[pos[0]],
|
||||
[pos[1]],
|
||||
s=size,
|
||||
color=color,
|
||||
edgecolors=edgecolor,
|
||||
linewidths=0.4,
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
if ann_positions:
|
||||
ax.scatter(
|
||||
[p[0] for p in ann_positions.values()],
|
||||
[p[1] for p in ann_positions.values()],
|
||||
s=9,
|
||||
color="black",
|
||||
zorder=4,
|
||||
)
|
||||
|
||||
xs = [p[0] for p in list(ann_positions.values()) + list(apex_positions.values())]
|
||||
ys = [p[1] for p in list(ann_positions.values()) + list(apex_positions.values())]
|
||||
if connect_shared:
|
||||
ys.append(max(ys) + 1.2)
|
||||
ys.append(min(ys) - 1.2)
|
||||
xpad = 1.1 if connect_shared else 1.7
|
||||
ypad = 0.25 if connect_shared else 0.0
|
||||
ax.set_xlim(min(xs, default=min(offsets, default=0.0)) - xpad, max(xs, default=max(offsets, default=0.0)) + xpad)
|
||||
ax.set_ylim(min(ys, default=-1.65) - 0.25, max(ys, default=1.65) + ypad)
|
||||
ax.set_aspect("equal")
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
def draw_tread_model(ax, node: TreadNode):
|
||||
if len(node.annular_cycles) > 1:
|
||||
draw_tread_cycles(ax, node, connect_shared=True)
|
||||
singleton_down = set(node.down) - set(node.bites)
|
||||
ax.set_title(
|
||||
f"T{node.idx} d={node.depth}: {len(node.annular_cycles)} annular cycle(s)\n"
|
||||
f"ann={len(node.annular)} up={len(node.up)} down={len(singleton_down)} "
|
||||
f"bite={len(node.bites)}",
|
||||
fontsize=6.4,
|
||||
pad=1.5,
|
||||
)
|
||||
return
|
||||
|
||||
draw_tread_cycles(ax, node, connect_shared=False)
|
||||
|
||||
singleton_down = set(node.down) - set(node.bites)
|
||||
ax.set_title(
|
||||
f"T{node.idx} d={node.depth}: {len(node.annular_cycles)} annular cycle(s)\n"
|
||||
f"ann={len(node.annular)} up={len(node.up)} down={len(singleton_down)} "
|
||||
f"bite={len(node.bites)}",
|
||||
fontsize=6.4,
|
||||
pad=1.5,
|
||||
)
|
||||
|
||||
|
||||
def draw_medial_tire_grid(fig, outer_spec, nodes):
|
||||
if not nodes:
|
||||
ax = fig.add_subplot(outer_spec)
|
||||
ax.text(0.5, 0.5, "No medial treads extracted", ha="center")
|
||||
ax.axis("off")
|
||||
return
|
||||
cols = min(3, max(1, math.ceil(math.sqrt(len(nodes)))))
|
||||
rows = math.ceil(len(nodes) / cols)
|
||||
sub = outer_spec.subgridspec(rows, cols, wspace=0.08, hspace=0.35)
|
||||
for i in range(rows * cols):
|
||||
ax = fig.add_subplot(sub[i // cols, i % cols])
|
||||
if i < len(nodes):
|
||||
draw_tread_model(ax, nodes[i])
|
||||
else:
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
def write_index(
|
||||
path: Path,
|
||||
graph_idx: int,
|
||||
source: int,
|
||||
original_graph: nx.Graph,
|
||||
augmentation: Augmentation,
|
||||
nodes,
|
||||
tree_edges,
|
||||
):
|
||||
g = augmentation.graph
|
||||
lines = [
|
||||
f"# Random medial tire decomposition {graph_idx}",
|
||||
"",
|
||||
f"- original vertices: {original_graph.number_of_nodes()}",
|
||||
f"- original edges: {original_graph.number_of_edges()}",
|
||||
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"- tire-tree nodes: {len(nodes)}",
|
||||
f"- tire-tree edges: {len(tree_edges)}",
|
||||
"",
|
||||
"| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |",
|
||||
"|--:|--:|--:|--:|--:|--:|--:|--:|",
|
||||
]
|
||||
for node in nodes:
|
||||
singleton_down = set(node.down) - set(node.bites)
|
||||
lines.append(
|
||||
f"| T{node.idx} | {node.depth} | {len(node.face_indices)} | "
|
||||
f"{len(node.annular_cycles)} | {len(node.annular)} | {len(node.up)} | "
|
||||
f"{len(singleton_down)} | {len(node.bites)} |"
|
||||
)
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def draw_case(out_dir: Path, graph_idx: int, g: nx.Graph, source: int, augment: bool = True):
|
||||
augmentation, _faces, levels, nodes, tree_edges = build_tire_tree(g, source, augment=augment)
|
||||
work_graph = augmentation.graph
|
||||
fig = plt.figure(figsize=(17, 10))
|
||||
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_tree = fig.add_subplot(spec[1, 0])
|
||||
draw_base_graph(ax_graph, work_graph, levels, source, augmentation.added_vertices)
|
||||
draw_tire_tree(ax_tree, nodes, tree_edges)
|
||||
draw_medial_tire_grid(fig, spec[:, 1], nodes)
|
||||
fig.suptitle(
|
||||
f"Random 5-connected maximal planar graph {graph_idx}: "
|
||||
f"n={g.number_of_nodes()} (+{len(augmentation.added_vertices)}), "
|
||||
f"source={source}",
|
||||
fontsize=13,
|
||||
)
|
||||
legend = [
|
||||
Line2D([0], [0], marker="o", color="w", label="source",
|
||||
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="#475569", lw=0.8, linestyle=(0, (1.2, 2.0)),
|
||||
label="shared up apex"),
|
||||
Line2D([0], [0], marker="o", color="w", label="up tooth",
|
||||
markerfacecolor="#2563eb", markersize=6),
|
||||
Line2D([0], [0], marker="o", color="w", label="down tooth",
|
||||
markerfacecolor="#dc2626", markersize=6),
|
||||
Line2D([0], [0], marker="o", color="w", label="bite apex",
|
||||
markerfacecolor="#7f1d1d", markeredgecolor="black", markersize=6),
|
||||
]
|
||||
fig.legend(handles=legend, loc="lower center", ncol=6, fontsize=9)
|
||||
fig.subplots_adjust(left=0.03, right=0.99, top=0.92, bottom=0.08, wspace=0.08, hspace=0.16)
|
||||
|
||||
png = out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.png"
|
||||
pdf = out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.pdf"
|
||||
fig.savefig(png, dpi=180)
|
||||
fig.savefig(pdf)
|
||||
plt.close(fig)
|
||||
write_index(
|
||||
out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.md",
|
||||
graph_idx,
|
||||
source,
|
||||
g,
|
||||
augmentation,
|
||||
nodes,
|
||||
tree_edges,
|
||||
)
|
||||
return png, pdf, len(nodes), sum(len(node.annular_cycles) for node in nodes)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace):
|
||||
out_dir = Path(args.out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.graph6:
|
||||
graph = nx.from_graph6_bytes(args.graph6.encode())
|
||||
if nx.node_connectivity(graph) < 5:
|
||||
raise ValueError("--graph6 graph must be 5-connected")
|
||||
graphs = [graph]
|
||||
if args.source is None:
|
||||
raise ValueError("--source is required with --graph6")
|
||||
sources = [args.source]
|
||||
else:
|
||||
graphs = sample_plantri_graphs(args.n, args.count, args.seed, args.scan_limit)
|
||||
rng = random.Random(args.seed + 101)
|
||||
sources = [rng.choice(list(graph.nodes())) for graph in graphs]
|
||||
|
||||
for i, (graph, source) in enumerate(zip(graphs, sources), start=1):
|
||||
png, pdf, node_count, annular_cycle_count = draw_case(
|
||||
out_dir, i, graph, source, augment=not args.no_augment_same_level_faces
|
||||
)
|
||||
print(
|
||||
f"case {i}: source={source}, connectivity={nx.node_connectivity(graph)}, "
|
||||
f"tire nodes={node_count}, annular cycles={annular_cycle_count}"
|
||||
)
|
||||
print(f" wrote {png}")
|
||||
print(f" wrote {pdf}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--n", type=int, default=30)
|
||||
parser.add_argument("--count", type=int, default=2)
|
||||
parser.add_argument("--seed", type=int, default=20260615)
|
||||
parser.add_argument("--scan-limit", type=int, default=500)
|
||||
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(
|
||||
"--no-augment-same-level-faces",
|
||||
action="store_true",
|
||||
help="skip the same-level-face vertex insertion step",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-dir",
|
||||
default=str(PAPER_DIR / "experiments" / "random_medial_tire_decompositions"),
|
||||
)
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Exhaustive generator for full medial tire graphs, indexed by |A(T)|.
|
||||
|
||||
Model (Definitions/Remarks 3.1--3.9 of the medial tire decompositions paper).
|
||||
|
||||
* The annular medial vertices induce a cycle A(T), the *annular cycle*
|
||||
(Theorem 3.3). Write n = |A(T)| for its number of vertices = number of
|
||||
annular faces = number of annular edges e_0,...,e_{n-1}.
|
||||
|
||||
* Each edge e_i of A(T) carries exactly one tooth (a triangle of M(T))
|
||||
whose third vertex is a non-annular apex (Definition 3.4). A tooth is an
|
||||
*up tooth* (apex in the outer region) or a *down tooth* (apex in the inner
|
||||
region). We record the tooth types as a word in {U, D}^n.
|
||||
|
||||
* No two up teeth share an apex; at most two down teeth share an apex
|
||||
(Remark 3.5). Two down teeth sharing an apex form a *bite* (Definition
|
||||
3.7). So the down teeth are partitioned into singletons and bite pairs.
|
||||
A bite pairs two down-edges and is drawn as an apex inside the disk with
|
||||
spokes to the four endpoints; bites must be mutually non-crossing, i.e.
|
||||
the bite pairs form a non-crossing (laminar) matching of the down-edges.
|
||||
The two annular edges of a bite must be non-incident (Definition 3.7):
|
||||
they share no annular vertex, so cyclically adjacent edges cannot pair.
|
||||
|
||||
* There are at least three up teeth (Remark 3.6).
|
||||
|
||||
* Bite-face condition (Remark 3.8). Let B(T) = A(T) together with the bite
|
||||
apexes. Its interior non-tooth faces are the root face plus one inner-gap
|
||||
face per bite. A singleton down tooth lies in the innermost bite enclosing
|
||||
its edge (or in the root face if none). For every interior non-tooth face
|
||||
the number of down-tooth apexes lying in that face must be 0 or at least 3.
|
||||
Equivalently: no face holds exactly one or two singleton down teeth.
|
||||
|
||||
The generator enumerates, for a given n, every (tooth word, bite matching)
|
||||
pair satisfying these properties and emits the resulting full medial tire
|
||||
graph as an explicit vertex/edge structure. Configurations may optionally be
|
||||
reduced modulo the dihedral symmetry of the cycle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Iterator
|
||||
|
||||
# A bite is an unordered pair of down-edge indices (i, j) with i < j.
|
||||
Bite = tuple[int, int]
|
||||
Matching = frozenset[Bite]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-crossing (laminar) matchings of the down edges.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def noncrossing_matchings(positions: tuple[int, ...]) -> tuple[Matching, ...]:
|
||||
"""All non-crossing partial matchings of ``positions`` (sorted ascending).
|
||||
|
||||
Bite pairs drawn inside the disk are non-crossing iff, read in cyclic
|
||||
order, no two pairs interleave. Cutting the cycle at the gap before the
|
||||
first edge turns this into ordinary non-crossing interval matchings, which
|
||||
obey the Catalan recursion below.
|
||||
"""
|
||||
if not positions:
|
||||
return (frozenset(),)
|
||||
head, *rest = positions
|
||||
out: list[Matching] = []
|
||||
# head left unmatched (a singleton down tooth, if its edge is down)
|
||||
for tail in noncrossing_matchings(tuple(rest)):
|
||||
out.append(tail)
|
||||
# head matched with positions[k]; the strictly-enclosed block must be
|
||||
# matched within itself to stay non-crossing.
|
||||
for k in range(1, len(positions)):
|
||||
partner = positions[k]
|
||||
inside = tuple(positions[1:k])
|
||||
outside = tuple(positions[k + 1:])
|
||||
for m_in in noncrossing_matchings(inside):
|
||||
for m_out in noncrossing_matchings(outside):
|
||||
out.append(frozenset({(head, partner)}) | m_in | m_out)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The bite-face condition (Remark 3.8).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def incident_edges(i: int, j: int, n: int) -> bool:
|
||||
"""Whether annular edges i and j share an annular vertex on the n-cycle."""
|
||||
return (j - i) % n == 1 or (i - j) % n == 1
|
||||
|
||||
|
||||
def has_incident_bite(bites: Matching, n: int) -> bool:
|
||||
"""Whether any bite pairs two incident (cyclically adjacent) edges."""
|
||||
return any(incident_edges(i, j, n) for i, j in bites)
|
||||
|
||||
|
||||
def innermost_bite(edge: int, bites: Matching) -> Bite | None:
|
||||
"""The minimal-span bite whose open interval contains ``edge``, or None."""
|
||||
enclosing = [b for b in bites if b[0] < edge < b[1]]
|
||||
if not enclosing:
|
||||
return None
|
||||
return min(enclosing, key=lambda b: b[1] - b[0])
|
||||
|
||||
|
||||
def face_singleton_counts(
|
||||
tooth_word: str, bites: Matching
|
||||
) -> dict[Bite | None, int]:
|
||||
"""Down-singletons per interior non-tooth face of B(T).
|
||||
|
||||
The key ``None`` is the root face; a bite key is that bite's inner-gap
|
||||
face. Faces with no singletons are simply absent from the result.
|
||||
"""
|
||||
matched = {edge for pair in bites for edge in pair}
|
||||
counts: dict[Bite | None, int] = defaultdict(int)
|
||||
for edge, tooth in enumerate(tooth_word):
|
||||
if tooth != "D" or edge in matched:
|
||||
continue # only singleton down teeth contribute apexes
|
||||
counts[innermost_bite(edge, bites)] += 1
|
||||
return dict(counts)
|
||||
|
||||
|
||||
def satisfies_bite_face_condition(tooth_word: str, bites: Matching) -> bool:
|
||||
"""Remark 3.8: every non-tooth face holds 0 or >=3 down-tooth apexes."""
|
||||
return all(count >= 3 for count in face_singleton_counts(tooth_word, bites).values())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The full medial tire graph as an explicit object.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FullMedialTireGraph:
|
||||
"""A full medial tire graph M(T) determined by its combinatorial data.
|
||||
|
||||
Vertices are named:
|
||||
a{k} annular medial vertex k (k = 0..n-1), forming A(T);
|
||||
u{i} apex of the up tooth on edge i;
|
||||
d{i} apex of the singleton down tooth on edge i;
|
||||
p{i}_{j} apex of the bite pairing edges i and j (i < j).
|
||||
"""
|
||||
|
||||
n: int
|
||||
tooth_word: str
|
||||
bites: Matching
|
||||
|
||||
@property
|
||||
def up_edges(self) -> tuple[int, ...]:
|
||||
return tuple(i for i, t in enumerate(self.tooth_word) if t == "U")
|
||||
|
||||
@property
|
||||
def down_edges(self) -> tuple[int, ...]:
|
||||
return tuple(i for i, t in enumerate(self.tooth_word) if t == "D")
|
||||
|
||||
@property
|
||||
def bite_edges(self) -> frozenset[int]:
|
||||
return frozenset(edge for pair in self.bites for edge in pair)
|
||||
|
||||
@property
|
||||
def singleton_down_edges(self) -> tuple[int, ...]:
|
||||
bite = self.bite_edges
|
||||
return tuple(i for i in self.down_edges if i not in bite)
|
||||
|
||||
def apex_of_edge(self, edge: int) -> str:
|
||||
if self.tooth_word[edge] == "U":
|
||||
return f"u{edge}"
|
||||
for i, j in self.bites:
|
||||
if edge in (i, j):
|
||||
return f"p{i}_{j}"
|
||||
return f"d{edge}"
|
||||
|
||||
def vertices(self) -> list[str]:
|
||||
verts = [f"a{k}" for k in range(self.n)]
|
||||
for i in self.up_edges:
|
||||
verts.append(f"u{i}")
|
||||
for i in self.singleton_down_edges:
|
||||
verts.append(f"d{i}")
|
||||
for i, j in sorted(self.bites):
|
||||
verts.append(f"p{i}_{j}")
|
||||
return verts
|
||||
|
||||
def edges(self) -> list[tuple[str, str]]:
|
||||
n = self.n
|
||||
out: list[tuple[str, str]] = []
|
||||
# annular cycle A(T)
|
||||
for k in range(n):
|
||||
out.append((f"a{k}", f"a{(k + 1) % n}"))
|
||||
# singleton teeth (up and down): two spokes each
|
||||
for i in self.up_edges:
|
||||
out += [(f"u{i}", f"a{i}"), (f"u{i}", f"a{(i + 1) % n}")]
|
||||
for i in self.singleton_down_edges:
|
||||
out += [(f"d{i}", f"a{i}"), (f"d{i}", f"a{(i + 1) % n}")]
|
||||
# bites: a shared apex with four spokes
|
||||
for i, j in sorted(self.bites):
|
||||
apex = f"p{i}_{j}"
|
||||
for edge in (i, j):
|
||||
out += [(apex, f"a{edge}"), (apex, f"a{(edge + 1) % n}")]
|
||||
return [tuple(sorted(e)) for e in out]
|
||||
|
||||
def canonical_key(self) -> tuple:
|
||||
"""Representative under the dihedral group of the cycle (rotations and
|
||||
reflections), so symmetric configurations collapse to one key."""
|
||||
n = self.n
|
||||
best: tuple | None = None
|
||||
for a in (1, -1):
|
||||
for b in range(n):
|
||||
relabel = lambda i: (a * i + b) % n
|
||||
word = [""] * n
|
||||
for i, t in enumerate(self.tooth_word):
|
||||
word[relabel(i)] = t
|
||||
mapped = tuple(sorted(
|
||||
tuple(sorted((relabel(i), relabel(j)))) for i, j in self.bites
|
||||
))
|
||||
key = (tuple(word), mapped)
|
||||
if best is None or key < best:
|
||||
best = key
|
||||
return best
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enumeration.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate(
|
||||
n: int, min_up_teeth: int = 3, dedup: bool = False
|
||||
) -> Iterator[FullMedialTireGraph]:
|
||||
"""Yield every full medial tire graph whose annular cycle has size ``n``.
|
||||
|
||||
``min_up_teeth`` defaults to 3 (Remark 3.6). With ``dedup`` set, only one
|
||||
representative per dihedral symmetry class is returned.
|
||||
"""
|
||||
seen: set[tuple] = set()
|
||||
for word_tuple in itertools.product("UD", repeat=n):
|
||||
tooth_word = "".join(word_tuple)
|
||||
if tooth_word.count("U") < min_up_teeth:
|
||||
continue
|
||||
down = tuple(i for i, t in enumerate(tooth_word) if t == "D")
|
||||
for bites in noncrossing_matchings(down):
|
||||
if has_incident_bite(bites, n):
|
||||
continue
|
||||
if not satisfies_bite_face_condition(tooth_word, bites):
|
||||
continue
|
||||
graph = FullMedialTireGraph(n=n, tooth_word=tooth_word, bites=bites)
|
||||
if dedup:
|
||||
key = graph.canonical_key()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
yield graph
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def figure_one() -> FullMedialTireGraph:
|
||||
"""The example graph of Figure 1 (Remark 3.8): 12 edges, one bite (0,6)."""
|
||||
return FullMedialTireGraph(
|
||||
n=12,
|
||||
tooth_word="DDDDDUDUUUUU", # edges 0-4,6 down; 5,7,8,9,10,11 up
|
||||
bites=frozenset({(0, 6)}),
|
||||
)
|
||||
|
||||
|
||||
def describe(graph: FullMedialTireGraph) -> str:
|
||||
counts = face_singleton_counts(graph.tooth_word, graph.bites)
|
||||
face_strs = []
|
||||
for face, c in sorted(counts.items(), key=lambda kv: (kv[0] is not None, kv[0])):
|
||||
name = "root" if face is None else f"bite{face}"
|
||||
face_strs.append(f"{name}:{c}")
|
||||
bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-"
|
||||
faces = " ".join(face_strs) or "-"
|
||||
return (
|
||||
f"word={graph.tooth_word} up={len(graph.up_edges)} "
|
||||
f"down={len(graph.down_edges)} bites={bites} faces[{faces}]"
|
||||
)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
if args.check_figure:
|
||||
g = figure_one()
|
||||
print("Figure 1 check:")
|
||||
print(f" {describe(g)}")
|
||||
ok = satisfies_bite_face_condition(g.tooth_word, g.bites)
|
||||
print(f" satisfies Remark 3.8: {ok} (expect True; faces 4 and 0)")
|
||||
print()
|
||||
|
||||
for n in range(args.min_n, args.max_n + 1):
|
||||
graphs = list(generate(n, min_up_teeth=args.min_up, dedup=args.dedup))
|
||||
label = "classes" if args.dedup else "graphs"
|
||||
print(f"n={n}: {len(graphs)} {label}")
|
||||
if args.show:
|
||||
for g in graphs[: args.show]:
|
||||
print(f" {describe(g)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--min-n", type=int, default=3)
|
||||
parser.add_argument("--max-n", type=int, default=8)
|
||||
parser.add_argument("--min-up", type=int, default=3, help="Remark 3.6 bound")
|
||||
parser.add_argument("--dedup", action="store_true",
|
||||
help="reduce modulo dihedral symmetry of the cycle")
|
||||
parser.add_argument("--show", type=int, default=0,
|
||||
help="print up to this many graphs per n")
|
||||
parser.add_argument("--check-figure", action="store_true",
|
||||
help="verify the Figure 1 example against Remark 3.8")
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Medial tire decomposition helpers for plane triangulation experiments."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from .full_medial_tire_generator import FullMedialTireGraph
|
||||
|
||||
|
||||
def ekey(u, v):
|
||||
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
|
||||
|
||||
|
||||
def medial_tire_facemodel(tread_faces) -> nx.Graph:
|
||||
"""Build the ambient tread-face model of a full medial tire graph M(T)."""
|
||||
mt = nx.Graph()
|
||||
for f in tread_faces:
|
||||
es = [ekey(f[0], f[1]), ekey(f[1], f[2]), ekey(f[2], f[0])]
|
||||
mt.add_nodes_from(es)
|
||||
for a in range(3):
|
||||
mt.add_edge(es[a], es[(a + 1) % 3])
|
||||
return mt
|
||||
|
||||
|
||||
def extract_tread(faces, levels, d):
|
||||
"""Tread T_d: faces spanning levels {d, d+1}. Return its edge classes."""
|
||||
tread_faces = []
|
||||
for f in faces:
|
||||
lv = [levels[x] for x in f]
|
||||
if min(lv) == d and max(lv) == d + 1:
|
||||
tread_faces.append(f)
|
||||
if not tread_faces:
|
||||
return None
|
||||
|
||||
annular, up, down = set(), set(), set()
|
||||
face_of_down = defaultdict(int)
|
||||
for f in tread_faces:
|
||||
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
|
||||
e = ekey(x, y)
|
||||
lx, ly = levels[x], levels[y]
|
||||
if {lx, ly} == {d, d + 1}:
|
||||
annular.add(e)
|
||||
elif lx == ly == d:
|
||||
up.add(e)
|
||||
elif lx == ly == d + 1:
|
||||
down.add(e)
|
||||
face_of_down[e] += 1
|
||||
|
||||
bites = {e for e in down if face_of_down[e] == 2}
|
||||
return {
|
||||
"tread_faces": tread_faces,
|
||||
"annular": annular,
|
||||
"up": up,
|
||||
"down": down,
|
||||
"bites": bites,
|
||||
}
|
||||
|
||||
|
||||
def _cycle_order(sub: nx.Graph, comp):
|
||||
"""Cyclic order of a simple 2-regular component, or None."""
|
||||
csub = sub.subgraph(comp)
|
||||
if csub.number_of_nodes() < 3 or any(csub.degree(v) != 2 for v in csub):
|
||||
return None
|
||||
start = next(iter(comp))
|
||||
order = [start]
|
||||
prev, cur = None, start
|
||||
while True:
|
||||
nbrs = [w for w in csub.neighbors(cur) if w != prev]
|
||||
if not nbrs:
|
||||
break
|
||||
nxt = nbrs[0]
|
||||
if nxt == start:
|
||||
break
|
||||
order.append(nxt)
|
||||
prev, cur = cur, nxt
|
||||
return order if len(order) == csub.number_of_nodes() else None
|
||||
|
||||
|
||||
def annular_cycle_order(M: nx.Graph, annular: set):
|
||||
"""Cyclic order of annular medial vertices when they induce one cycle."""
|
||||
sub = M.subgraph(annular)
|
||||
if not annular or not nx.is_connected(sub):
|
||||
return None
|
||||
return _cycle_order(sub, set(annular))
|
||||
|
||||
|
||||
def annular_cycle_components(M: nx.Graph, annular: set):
|
||||
"""Cyclic orders of annular medial vertices, one per cycle component."""
|
||||
sub = M.subgraph(annular)
|
||||
orders = []
|
||||
for comp in nx.connected_components(sub):
|
||||
order = _cycle_order(sub, comp)
|
||||
if order is not None:
|
||||
orders.append(order)
|
||||
return orders
|
||||
|
||||
|
||||
def _linear_cut(n, bite_pairs):
|
||||
"""Rotate the cycle so bite pairs become linear non-crossing intervals."""
|
||||
for r in range(n):
|
||||
rel = [tuple(sorted(((i - r) % n, (j - r) % n))) for i, j in bite_pairs]
|
||||
ok = True
|
||||
for a, b in rel:
|
||||
for c, d in rel:
|
||||
if (a, b) != (c, d) and (a < c < b < d or c < a < d < b):
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
break
|
||||
if ok:
|
||||
return r, rel
|
||||
return None
|
||||
|
||||
|
||||
def _recognise_one(M, order, up, ann_global):
|
||||
"""Recognise one annular cycle as a FullMedialTireGraph."""
|
||||
n = len(order)
|
||||
if n < 3:
|
||||
return None
|
||||
ann_set = set(order)
|
||||
|
||||
apex_of_edge = []
|
||||
for i in range(n):
|
||||
a, b = order[i], order[(i + 1) % n]
|
||||
common = [
|
||||
w for w in set(M.neighbors(a)) & set(M.neighbors(b))
|
||||
if w not in ann_global
|
||||
]
|
||||
if len(common) != 1:
|
||||
return None
|
||||
apex_of_edge.append(common[0])
|
||||
|
||||
apex_positions = defaultdict(list)
|
||||
for i, ap in enumerate(apex_of_edge):
|
||||
apex_positions[ap].append(i)
|
||||
bite_pairs = [
|
||||
tuple(sorted(positions))
|
||||
for positions in apex_positions.values()
|
||||
if len(positions) == 2
|
||||
]
|
||||
tooth = ["U" if ap in up else "D" for ap in apex_of_edge]
|
||||
|
||||
cut = _linear_cut(n, bite_pairs)
|
||||
if cut is None:
|
||||
return None
|
||||
r, rel_bites = cut
|
||||
word = [""] * n
|
||||
for i in range(n):
|
||||
word[(i - r) % n] = tooth[i]
|
||||
graph = FullMedialTireGraph(
|
||||
n=n, tooth_word="".join(word), bites=frozenset(rel_bites)
|
||||
)
|
||||
|
||||
bij = {}
|
||||
for k in range(n):
|
||||
bij[f"a{k}"] = order[(k + r) % n]
|
||||
for i in graph.up_edges:
|
||||
bij[f"u{i}"] = apex_of_edge[(i + r) % n]
|
||||
for i in graph.singleton_down_edges:
|
||||
bij[f"d{i}"] = apex_of_edge[(i + r) % n]
|
||||
for i, j in sorted(graph.bites):
|
||||
bij[f"p{i}_{j}"] = apex_of_edge[(i + r) % n]
|
||||
|
||||
sub_nodes = ann_set | set(apex_of_edge)
|
||||
sub_edges = {ekey(*e) for e in M.subgraph(sub_nodes).edges()}
|
||||
rec_edges = {ekey(bij[u], bij[v]) for u, v in graph.edges()}
|
||||
if rec_edges != sub_edges:
|
||||
return None
|
||||
return graph, bij
|
||||
|
||||
|
||||
def recognise(M, tread):
|
||||
"""Recognise the tread's medial-tire structure.
|
||||
|
||||
A tread's annular frontier may be several disjoint cycles, each its own
|
||||
full medial tire graph. Returns one ``(FullMedialTireGraph, bijection)``
|
||||
pair per annular cycle component that recognises.
|
||||
"""
|
||||
up = set(tread["up"])
|
||||
ann_global = set(tread["annular"])
|
||||
tires = []
|
||||
for order in annular_cycle_components(M, tread["annular"]):
|
||||
rec = _recognise_one(M, order, up, ann_global)
|
||||
if rec is not None:
|
||||
tires.append(rec)
|
||||
return tires
|
||||
@@ -12,33 +12,33 @@
|
||||
\newlabel{def:full-medial-tire}{{3.1}{2}}
|
||||
\newlabel{thm:annular-medial-colour-bound}{{3.3}{3}}
|
||||
\newlabel{def:annular-teeth}{{3.4}{3}}
|
||||
\newlabel{rem:teeth-sharing}{{3.5}{3}}
|
||||
\newlabel{rem:up-teeth-count}{{3.6}{3}}
|
||||
\citation{bauerfeld-nested-tire-decompositions}
|
||||
\citation{bauerfeld-nested-tire-decompositions}
|
||||
\newlabel{rem:teeth-sharing}{{3.5}{4}}
|
||||
\newlabel{rem:up-teeth-count}{{3.6}{4}}
|
||||
\newlabel{def:bite}{{3.7}{4}}
|
||||
\newlabel{rem:bite-face-count}{{3.8}{4}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A full medial tire graph $\mathsf {M}(T)$ illustrating the tooth terminology. The thick cycle is the annular medial cycle $A(T)$, whose black vertices are the annular medial vertices. Each edge of $A(T)$ carries one tooth: up teeth (blue apexes, outer-boundary medial vertices) point into the outer region, and down teeth (red apexes, inner-boundary medial vertices) point into the inner region. The two down teeth meeting at the central shared apex (larger red vertex) form a bite; that shared apex splits the inner region into two faces, one with four down teeth on its boundary and one with none.}}{4}{}\protected@file@percent }
|
||||
\newlabel{fig:medial-teeth-example}{{1}{4}}
|
||||
\newlabel{def:boundary-medial-vertices}{{3.9}{4}}
|
||||
\citation{bauerfeld-nested-tire-decompositions}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Three six-face full medial tire graphs found by the boundary-state restriction search. Black vertices are annular medial vertices; blue vertices are outer boundary medial vertices and red vertices are inner boundary medial vertices. The word below each diagram records the outer/inner type of the six annular faces in cyclic order. Boundary states are identified only up to colour permutation, not by rotation or reflection of the boundary order.}}{5}{}\protected@file@percent }
|
||||
\newlabel{def:medial-restriction-relation}{{3.10}{4}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{Decomposition}}{4}{}\protected@file@percent }
|
||||
\newlabel{cor:medial-tire-decomposition}{{4.1}{4}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A simple medial tire graph $\mathsf {M}(T)$ illustrating the tooth terminology. The thick cycle is the annular medial cycle $A(T)$, whose black vertices are the annular medial vertices. Each edge of $A(T)$ carries one tooth: up teeth (blue apexes, outer-boundary medial vertices) point into the outer region, and down teeth (red apexes, inner-boundary medial vertices) point into the inner region. The two down teeth meeting at the central shared apex (larger red vertex) form a bite; that shared apex splits the inner region into two faces, one with four down teeth on its boundary and one with none.}}{5}{}\protected@file@percent }
|
||||
\newlabel{fig:medial-teeth-example}{{1}{5}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Three six-face simple medial tire graphs found by the boundary-state restriction search. Black vertices are annular medial vertices; blue vertices are outer boundary medial vertices and red vertices are inner boundary medial vertices. The word below each diagram records the outer/inner type of the six annular faces in cyclic order. Boundary states are identified only up to colour permutation, not by rotation or reflection of the boundary order.}}{5}{}\protected@file@percent }
|
||||
\newlabel{fig:medial-restriction-worst-cases}{{2}{5}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces A proper vertex $3$-colouring of the full medial graph of the first seven-vertex counterexample found by the experiment. The medial vertex labelled $ij$ corresponds to the edge $(i,j)$ of the triangulation. For the vertex-source decomposition at source $1$, the highlighted annular medial cycle has colour counts $(2,2,2)$, so it is not coloured with two colours except at at most one vertex.}}{5}{}\protected@file@percent }
|
||||
\newlabel{fig:medial-annular-cycle-counterexample}{{3}{5}}
|
||||
\newlabel{def:medial-restriction-relation}{{3.10}{5}}
|
||||
\citation{bauerfeld-nested-tire-decompositions}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{Decomposition}}{6}{}\protected@file@percent }
|
||||
\newlabel{cor:medial-tire-decomposition}{{4.1}{6}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces A proper vertex $3$-colouring of the full medial graph of the first seven-vertex counterexample found by the experiment. The medial vertex labelled $ij$ corresponds to the edge $(i,j)$ of the triangulation. For the vertex-source decomposition at source $1$, the highlighted annular medial cycle has colour counts $(2,2,2)$, so it is not coloured with two colours except at at most one vertex.}}{6}{}\protected@file@percent }
|
||||
\newlabel{fig:medial-annular-cycle-counterexample}{{3}{6}}
|
||||
\newlabel{def:compatible-family}{{4.2}{6}}
|
||||
\newlabel{prop:gluing-criterion}{{4.3}{6}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{5}{A medial pigeonhole programme}}{6}{}\protected@file@percent }
|
||||
\newlabel{def:medial-boundary-state}{{5.1}{6}}
|
||||
\newlabel{def:medial-boundary-state}{{5.1}{7}}
|
||||
\newlabel{conj:medial-chain-pigeonhole}{{5.2}{7}}
|
||||
\newlabel{conj:medial-route-fct}{{5.3}{7}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{5.1}{Kempe-cycle conservation across medial tires}}{7}{}\protected@file@percent }
|
||||
\newlabel{lem:kempe-cycles}{{5.5}{7}}
|
||||
\newlabel{lem:kempe-conservation}{{5.6}{8}}
|
||||
\newlabel{def:kempe-balanced}{{5.7}{8}}
|
||||
\newlabel{rem:kempe-balance-necessary}{{5.8}{8}}
|
||||
\newlabel{rem:kempe-balance-necessary}{{5.8}{9}}
|
||||
\bibcite{bauerfeld-nested-tire-decompositions}{1}
|
||||
\bibcite{tait-original}{2}
|
||||
\newlabel{tocindent-1}{0pt}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Fdb version 3
|
||||
["pdflatex"] 1781210675 "paper.tex" "paper.pdf" "paper" 1781210676
|
||||
["pdflatex"] 1781554446 "paper.tex" "paper.pdf" "paper" 1781554447
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
|
||||
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
|
||||
@@ -132,8 +132,8 @@
|
||||
"/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 ""
|
||||
"/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
|
||||
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
|
||||
"paper.aux" 1781210676 4206 870862ca1c6762f39fd7ed9def109a09 "pdflatex"
|
||||
"paper.tex" 1781210650 40922 403b0b9df57192dbf02362b0b06705c3 ""
|
||||
"paper.aux" 1781554447 4210 3b62c5dd250f159f0e3d42ba3a3a7308 "pdflatex"
|
||||
"paper.tex" 1781554440 42216 46cb5902d4210f9324b1231139c3e122 ""
|
||||
(generated)
|
||||
"paper.aux"
|
||||
"paper.log"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 11 JUN 2026 16:44
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 15 JUN 2026 16:14
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
@@ -496,7 +496,7 @@ e
|
||||
))
|
||||
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
|
||||
[2] [3]
|
||||
Overfull \hbox (62.13657pt too wide) in paragraph at lines 363--372
|
||||
Overfull \hbox (62.13657pt too wide) in paragraph at lines 371--380
|
||||
[][]
|
||||
[]
|
||||
|
||||
@@ -504,17 +504,20 @@ Overfull \hbox (62.13657pt too wide) in paragraph at lines 363--372
|
||||
LaTeX Warning: `h' float specifier changed to `ht'.
|
||||
|
||||
|
||||
LaTeX Warning: `h' float specifier changed to `ht'.
|
||||
|
||||
|
||||
LaTeX Warning: `h' float specifier changed to `ht'.
|
||||
|
||||
[4] [5] [6] [7] [8] [9] [10] [11] (./paper.aux) )
|
||||
Here is how much of TeX's memory you used:
|
||||
14419 strings out of 478268
|
||||
283755 string characters out of 5846347
|
||||
609349 words of memory out of 5000000
|
||||
618029 words of memory out of 5000000
|
||||
32248 multiletter control sequences out of 15000+600000
|
||||
477048 words of font info for 58 fonts, out of 8000000 for 9000
|
||||
1302 hyphenation exceptions out of 8191
|
||||
84i,8n,89p,736b,838s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
84i,8n,89p,738b,838s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf
|
||||
-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-
|
||||
dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-di
|
||||
@@ -535,7 +538,7 @@ msfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
|
||||
sfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
|
||||
onts/cm/cmtt8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
|
||||
ts/symbols/msam10.pfb>
|
||||
Output written on paper.pdf (11 pages, 277538 bytes).
|
||||
Output written on paper.pdf (11 pages, 278916 bytes).
|
||||
PDF statistics:
|
||||
138 PDF objects out of 1000 (max. 8388607)
|
||||
86 compressed objects within 1 object stream
|
||||
|
||||
@@ -159,19 +159,27 @@ colours.
|
||||
|
||||
\section{Medial tire pieces}
|
||||
|
||||
\begin{definition}[Full medial tire graph]
|
||||
\begin{definition}[Simple and compound medial tire graphs]
|
||||
\label{def:full-medial-tire}
|
||||
Let $T$ be a tire tread in the tire tree $\mathcal{T}(G,S)$ supplied
|
||||
by~\cite{bauerfeld-nested-tire-decompositions}. The \emph{full medial
|
||||
tire graph} of $T$, denoted $\mathsf{M}(T)$, is the subgraph of
|
||||
$M(G)$ induced by the medial vertices $m_e$ with $e$ an edge of $G$
|
||||
incident to at least one triangular face in the tread $T$. The medial
|
||||
vertices corresponding to annular edges of $T$ are called
|
||||
\emph{annular medial vertices}.
|
||||
by~\cite{bauerfeld-nested-tire-decompositions}. The \emph{medial tire
|
||||
graph} of $T$, denoted $\mathsf{M}(T)$, is the subgraph of $M(G)$
|
||||
induced by the medial vertices $m_e$ with $e$ an edge of $G$ incident
|
||||
to at least one triangular face in the tread $T$. The medial vertices
|
||||
corresponding to annular edges of $T$ are called \emph{annular medial
|
||||
vertices}.
|
||||
|
||||
We call $\mathsf{M}(T)$ a \emph{simple medial tire graph} if its
|
||||
annular medial vertices induce a single cycle. We call
|
||||
$\mathsf{M}(T)$ a \emph{compound medial tire graph} if it is associated
|
||||
to a connected depth component of tread faces but its annular medial
|
||||
vertices induce more than one cycle. In a compound medial tire graph,
|
||||
annular teeth are understood cycle-by-cycle, and up-tooth apexes
|
||||
belonging to different annular cycles may coincide.
|
||||
\end{definition}
|
||||
|
||||
\begin{remark}
|
||||
In the ambient-triangulation setting, the full medial tire graph
|
||||
In the ambient-triangulation setting, the simple medial tire graph
|
||||
$\mathsf{M}(T)$ coincides with the omitted-edge medial tire graph
|
||||
studied in~\cite{bauerfeld-nested-tire-decompositions}. Indeed, the
|
||||
medial edges of $\mathsf{M}(T)$ are contributed by corners of annular
|
||||
@@ -361,7 +369,7 @@ interior.
|
||||
\node[lbl, anchor=east] at (192:1.86) (L0) {region with 0 down teeth};
|
||||
\draw[lead] (L0.east) -- (180:0.45);
|
||||
\end{tikzpicture}
|
||||
\caption{A full medial tire graph $\mathsf{M}(T)$ illustrating the tooth
|
||||
\caption{A simple medial tire graph $\mathsf{M}(T)$ illustrating the tooth
|
||||
terminology. The thick cycle is the annular medial cycle $A(T)$, whose
|
||||
black vertices are the annular medial vertices. Each edge of $A(T)$
|
||||
carries one tooth: up teeth (blue apexes, outer-boundary medial vertices)
|
||||
@@ -453,7 +461,7 @@ its boundary and one with none.}
|
||||
\node at (0,-2.15) {\scriptsize $\min |R_T(\alpha)|=1$};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\caption{Three six-face full medial tire graphs found by the boundary-state
|
||||
\caption{Three six-face simple medial tire graphs found by the boundary-state
|
||||
restriction search. Black vertices are annular medial vertices; blue
|
||||
vertices are outer boundary medial vertices and red vertices are inner
|
||||
boundary medial vertices. The word below each diagram records the
|
||||
@@ -601,9 +609,11 @@ parent and children.
|
||||
Let $G$ be a plane triangulation with level source $S$. The tire-tree
|
||||
decomposition $\mathcal{T}(G,S)$ of
|
||||
\cite{bauerfeld-nested-tire-decompositions} induces a rooted
|
||||
decomposition of the full medial graph $M(G)$ into full medial tire
|
||||
graphs $\{\mathsf{M}(T): T \in V(\mathcal{T}(G,S))\}$, glued along
|
||||
their boundary medial vertex sets.
|
||||
decomposition of the full medial graph $M(G)$ into medial tire graphs
|
||||
$\{\mathsf{M}(T): T \in V(\mathcal{T}(G,S))\}$, glued along their
|
||||
boundary medial vertex sets. A node of this decomposition may be a
|
||||
simple medial tire graph or a compound medial tire graph, depending on
|
||||
whether its annular medial vertices induce one cycle or several.
|
||||
\end{corollary}
|
||||
|
||||
\begin{proof}
|
||||
@@ -670,6 +680,17 @@ this framework would follow from a structural reason that these
|
||||
restriction sets cannot remain mutually disjoint along every branch of
|
||||
the tire tree.
|
||||
|
||||
In this chaining step, the inner side of a parent simple medial tire is
|
||||
read by its singleton down-tooth apex vertices. If the child side is a
|
||||
compound medial tire, then the parent's singleton down-tooth apex
|
||||
vertices are incident to---indeed, are identified with---the up-tooth
|
||||
apex vertices of the compound medial tire, interpreted cycle-by-cycle
|
||||
on its annular medial cycles. Equivalently, the primal edges
|
||||
represented by the parent's singleton down-tooth apexes are exactly the
|
||||
level-cycle interface edges represented on the child side as up-tooth
|
||||
apexes. This is the boundary identification along which the medial
|
||||
boundary states are chained.
|
||||
|
||||
\begin{definition}[Medial boundary state]
|
||||
\label{def:medial-boundary-state}
|
||||
A \emph{medial boundary state} on a boundary set
|
||||
@@ -786,12 +807,12 @@ $C$. Thus every entrance through $C$ is paired with an exit through
|
||||
$C$.
|
||||
\end{proof}
|
||||
|
||||
We now use these Kempe cycles to single out the colourings of a full
|
||||
We now use these Kempe cycles to single out the colourings of a simple
|
||||
medial tire graph that respect the annular tooth structure.
|
||||
|
||||
\begin{definition}[Kempe-balanced colouring]
|
||||
\label{def:kempe-balanced}
|
||||
Let $\varphi$ be a proper $3$-colouring of the full medial tire graph
|
||||
Let $\varphi$ be a proper $3$-colouring of the simple medial tire graph
|
||||
$\mathsf{M}(T)$. For a colour pair $P=\{a,b\}$, let $\mathsf{M}(T)_P$ be
|
||||
the subgraph induced by the vertices of colours $a$ and $b$. Since
|
||||
$\mathsf{M}(T)$ need not be $4$-regular, the components of
|
||||
|
||||