78 Commits

Author SHA1 Message Date
didericis f0fdae11d4 Add 2+2 color-split outerplanar survey and decomposition figure
Sanity check for the nested-outerplanar-shells construction: every
maximal planar graph through n=11 (1249 triangulations) admits a proper
4-coloring whose colors split into two complementary pairs, each inducing
an outerplanar even-cycle (bipartite) subgraph. Disconnected halves are
allowed. The odd-bipyramid "failures" of the earlier all-six-pairs test
decompose correctly under the right 2+2 split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 04:19:58 -04:00
didericis d9007c8697 Add bridge-derived census (n=6..10) to the disjunction section
Cross-tabulate bridge-derived vs intertwining-tree coverage: the
bridge-derived share falls from 100% (n=6) to 62.7% (n=10), the
disjunction never relies on it alone, and the "neither" column is
identically zero throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:27:49 -04:00
didericis b1d681f39e Add self-flip-neighbor survey and write-up
Add experiments/self_flip_neighbor.py: enumerate the admissible flips of a
maximal planar graph and test each for isomorphism to the original, i.e. decide
whether G lies in its own flip neighborhood N(G). Supports a single graph6 input
or an order-range survey over min-degree-5 triangulations.

Add a "self-flip neighbors" section to paper.tex with the n=12..25 survey table
and a remark: the self-flip fraction declines as expected (min-degree-5
triangulations are asymptotically rigid), and this is not useful for narrowing
the minimal criminal since it neither includes nor excludes any candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:59:44 -04:00
didericis f54b66f857 Add double-contraction reductio strategy note
Records the degree-5 double-contraction proof skeleton: Kempe chains as
Heawood face-chains (flip-set + same-side sign rule), the chain-transport
hypothesis L1, the inner/outer nested reductio, H'' as per-interface
contraction along the chain, and the single emptiness Claim it reduces to,
with the Errera oracle as the stress test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:43:45 -04:00
didericis 5552e07803 Add force-first Heawood labelling to the medial tire dual-cut experiment
heawood_labelling(): depth-seeded force-first +/-1 labelling of the
source-dual cut, targeting sum ≡ 0 (mod 3) on each dual face (vertex
link), with bookkeeping for seeds, forced fills, unforceable faces, and
failing faces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:43:21 -04:00
didericis 163e453464 Reframe the constraint floor honestly as a conjecture
Section 4 no longer states the floor as a proven Proposition. Now: prove
interior-free disks attain 2^(n-2) (ear-peeling) and the un-stacking
lemma, state |Phi(D)| >= 2^(n-2) as a Conjecture, and give an honest
status remark -- holds for the Apollonian class, reduces to the
irreducible case, empirically strict (5/4), but |Phi| is NOT monotone
(the earlier freedom-positive monotonicity claim was wrong) and both
natural elementary proofs provably fail. Soften the note's observation to
match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:32:57 -04:00
didericis c482bc5633 Option 2 (direct transversal) clean form is dead too
transversal.py: a STRONG transversal (n-2 faces whose Boolean assignment
single-valuedly and injectively determines the boundary) would give a
constructive proof. It exists in 0/2948 disks with k>=1 -- once there is
any interior vertex, fixing n-2 faces leaves boundary-visible completion
freedom, so the boundary is never single-valued in them. Works only at
k=0 (base case). Both elementary routes (reduction localization, direct
transversal) now closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:13:35 -04:00
didericis c4339624ce Strategy A localization fails: local star-vs-fan domination is false
local_star_fan.py: the size step |Phi(D-v)| <= |Phi(D)| localizes to a
star-vs-fan contribution comparison at v. But |Star(t)| >= min_root
|Fan(t)| is FALSE (6586 violations) -- the star's extra v-constraint
(sum mu ≡ 0) can make it realize fewer boundary vectors than the fan when
the link has interior vertices. So Strategy A is globally true (100%) but
NOT via per-vertex local domination; the size inequality needs a global
union/choice-of-v argument. Useful byproduct: the Boolean-bit / mod-3
incidence reformulation of Phi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:09:58 -04:00
didericis d7c93cf2c2 Test induction strategy for the irreducible lemma
reduction_exists.py: every disk with k>=1 (and every irreducible disk)
has a Phi-NON-INCREASING vertex removal (|Phi(D-v)| <= |Phi(D)|), 100%
over thousands of disks -- so induction to the k=0 base case is viable.
BUT the clean set-inclusion Phi(D-v) subset Phi(D) holds for only ~8%, so
the size step cannot be proved by "re-inserting v loses no sequence"; it
needs a genuine cardinality injection between non-nested sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:35:49 -04:00
didericis 411ff7f465 Add the lower-bound proof programme to the note
Section 4: un-stacking lemma (degree-3 removal preserves Phi, proved),
ear-peeling base case (k=0 => 2^(n-2)), reduction to the irreducible
case, and the irreducible lemma as the sole open conjecture (|Phi| >=
5/4 * 2^(n-2), tight at the degree-4/5 patch; wheel = floor(2^n/3) is not
extremal). Records the two dead ends (monotonicity false, universal
toggles insufficient) and ties each claim to its experiment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:27:36 -04:00
didericis 24a3d89d88 Pin the extremal irreducible disk (correcting the wheel claim)
wheel_extremal.py: |Phi(wheel_n)| = floor(2^n/3) exactly (ratio ->4/3),
but the wheel is NOT the irreducible minimiser for n>=6. The extremal disk
is a single MINIMAL-degree interior vertex (degree 4 or 5, both tie),
giving |Phi| = (5/4)*2^(n-2) = 5*2^(n-4). The ratio rises monotonically
with center degree, 5/4 -> 4/3, so minimal degree is extremal. Sharpens
the irreducible lemma to |Phi| >= (5/4)*2^(n-2), tight at the degree-4/5
patch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:14:52 -04:00
didericis bd8499a25b Isolate the irreducible case: reduces the floor proof to one lemma
irreducible_floor.py: over 10k+ irreducible disks (k>=1, min interior
degree >=4), |Phi| never violates 2^(n-2) and never sits on it -- min is
5*2^(n-4) = (5/4)2^(n-2), the wheel being the minimizer. Universal toggles
are dead (99.9% have zero boundary-only faces). Since un-stacking degree-3
vertices preserves Phi and terminates at a k=0 or irreducible residue, the
whole lower bound reduces to: every irreducible disk has |Phi| >= 2^(n-2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:08:00 -04:00
didericis 9f6328788c Test monotonicity lemma: degree-3 exact, but lemma is false at degree-4
monotonicity_test.py inserts interior vertices and checks |Phi|. Degree-3
stacks preserve Phi exactly (confirms un-stacking, 100%), but degree-4
insertions can SHRINK Phi (6->5, 30->28) and Phi(D') subset Phi(D) fails
~13% -- so the reduce-to-base-case proof of the 2^(n-2) floor via
monotonicity does not work. Violations stay above the floor, so the floor
is protected by something stronger; redirect to a direct n-2 toggle
construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 19:58:59 -04:00
didericis 1d981b4d01 Add the freedom-positive counting balance to the constraint floor
Remark: a disk with k interior vertices has 2k+n-2 faces (Euler) but only
k interior constraints, so each interior vertex adds two degrees of
freedom against one constraint -- depth is freedom-positive and Phi can
only retain or enlarge below the interior-free floor 2^(n-2). Motivates
the lower bound and replaces the prior TODO sketch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:24:04 -04:00
didericis b70ea2c087 Back the 2^(n-2) floor with validated diverse-disk search
The stacked-only search missed non-stacked disks, and cocircular boundary
points gave degenerate Delaunay (invalid disks, spurious sub-floor |Phi|).
Add floor_diverse_disks.py: 1700+ validated disks per n (convex non-
cocircular boundary, face-count and boundary-edge checks) confirm min|Phi|
= 2^(n-2). Note records that interior structure tends to ENLARGE Phi
(wheel 5 vs fan 4) and that depth adds two faces per one constraint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:22:41 -04:00
didericis d2156f06ee Scaffold the 2^(n-2) constraint-floor proposition
Add section 4: define the achievable boundary set Phi(D) of a triangulated
disk and state the constraint-floor proposition |Phi(D)| >= 2^(n-2), with
the attainment direction proved (fan injectivity) and the lower bound left
as a marked gap with strategy. Remark records the zonotope structure and
the short-interface concentration of difficulty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:14:52 -04:00
didericis 60c9f1d3a8 Add Heawood boundary-restriction experiments and findings note
Experiments probing the cluster restriction set R_K / Phi: R_K is a Z/3
zonotope (not a GF(3) subspace), the "richness" invariant is an artifact
of non-shrinking annuli, the interface gluing always works on interior
cycles (forced by 4CT), and the maximal constraint achievable on an
n-cycle is a floor of 2^(n-2) -- already reached by the trivial tire.
Note boundary_restriction_structure.tex writes these up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:12:54 -04:00
didericis 351ae0cdfe Account for the outer face in the Heawood face-sum identity
The bounded-face sum omits the outer face at outer-boundary vertices, so
restrict the gluing identity to interior vertices (where all cluster
interfaces live) and recover a colouring by carrying a single +/-1 label
on the unbounded face f_inf, giving Heawood's identity on the full cubic
dual for the Tait step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 01:33:07 -04:00
didericis c5f81842c7 Run Heawood pigeonhole between nested connected tire clusters
Add the two-sided cluster decomposition proposition: a vertex's full
Heawood face-sum splits as exactly one child-cluster contribution plus
one parent-cluster contribution (the at-most-two-clusters bound makes the
pairing binary and complete). Explain why this fails per-tire -- a vertex
on many same-depth tires has only a fragment of its face-star in any one
tire -- and recast the chain-pigeonhole and 4CT conjectures to nested
clusters with a cluster restriction relation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 01:10:30 -04:00
didericis 646cf9d12f Add connected tire clusters with two-cluster-per-vertex proposition
Define a connected tire cluster (union of same-depth tires joined by
shared vertices, transitive closure), prove same-depth tires meet only
in vertices, and prove every vertex lies in at most two clusters (one at
each of two consecutive depths) -- the bounded coarsening of the
unbounded per-vertex tire count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 01:03:37 -04:00
didericis 251c453437 Add Heawood chain-pigeonhole programme to tire-dual paper
Define a +/-1 Heawood face-labelling of a tire, its induced boundary
Heawood sequences and restriction relation, and interface compatibility
(0<->0, +1<->-1 = vertex face-sum vanishes mod 3). State the Heawood
chain-pigeonhole conjecture and a tire route to the Four Colour Theorem,
parallel to the medial programme.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:45:03 -04:00
didericis 851ca7fbed Scaffold Heawood restrictions on nested tire graph duals paper
Add a new paper stub referencing the nested tire decompositions paper,
with intro, Heawood bibliography entry, and an empty restrictions section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:31:48 -04:00
didericis af60c3b241 Draw source-dual cut on a valid straight-line embedding
Store the combinatorial planar embedding in the result and lay out the
source graph with nx.planar_layout so no primal edges cross and each dual
node sits inside its own triangle, replacing the concentric layout that
produced crossings. Add a committed generate_full_walk.py that reproduces
the walk .md/.pdf/.png outputs, and regenerate the walk 1 and walk 2 dual
figures and PDFs (reports unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 03:59:22 -04:00
didericis 4ba9ce47d1 Add walk-distance labelling to source-dual cut
Label each dual face by its distance, within the source-dual cut, from
the first entry's cap down tooth. Regenerate the seed 1 and seed 2 full
walk figures and metadata with the new labelling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 03:42:08 -04:00
didericis 696a6b3104 Add second full medial tire cut walk (seed 2)
Source-dual cut is a spanning tree of the dual (38 faces, 37 edges,
connected and acyclic) after 20 cut edges removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:56:30 -04:00
didericis 1e8bee04ce Handle compound medial tires in cut labelling 2026-06-15 21:46:56 -04:00
didericis 37a7ff0b00 Update medial tire cut labelling 2026-06-15 20:54:25 -04:00
didericis 9ef231655e Draw compound medial tires as separated cycles 2026-06-15 16:42:37 -04:00
didericis d541aea526 Move medial tire drawing script into lib 2026-06-15 16:25:33 -04:00
didericis 2a56322841 Define simple and compound medial tires 2026-06-15 16:14:50 -04:00
didericis 51c9efa7f2 Stop splitting random medial treads into components 2026-06-15 14:41:41 -04:00
didericis 464335082d Augment same-level faces before medial tire extraction 2026-06-15 14:35:13 -04:00
didericis 5829938ab0 Add branching medial tire decomposition example 2026-06-15 14:18:47 -04:00
didericis f537db9758 Draw random medial tire decompositions 2026-06-15 14:11:27 -04:00
didericis 6ef1dc710c Extract medial tire decomposition helpers 2026-06-15 14:05:17 -04:00
didericis b605931678 Treat each disjoint annular cycle as its own full medial tire graph
A tread's annular frontier can split into several disjoint cycles; each
is now recognised as a separate full medial tire graph instead of
disqualifying the whole tread.

- recognise() returns a list of (g, bij), one per annular cycle
  component; add annular_cycle_components() and _recognise_one(), and
  iterate components in iter_pieces().
- Key tires/results by (depth, component) throughout both experiment
  drivers: _label_treads chains each tire to a parent-depth down tooth
  sharing its apex; _cap_cut/_assemble_cut_graph/to_json/summary and the
  dual-cut collectors/draws follow suit.

Source vertex selection for the dual-cut experiment now deep-embeds a
random face and roots at the outer-cap vertex. The source-dual figure
labels the source-graph vertices, highlights the entry medial vertex,
and uses a cap-rooted concentric layout.

For seed 7 / face (14,15,19) this recognises treads 3 and 4 as two
tires each (3.0,3.1,4.0,4.1), so every dual face is now cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 12:01:36 -04:00
didericis 7554582056 Draw tread 0 (the source cap) in the dual-cut experiment
Add draw_cap_png and a --cap-png flag: render tread 0 as a wheel (source
hub, link-cycle rim, cap triangles filled, cap cut marked) from the
extract_tread roles, since tread 0 is skipped by tire recognition (a wheel
has no up teeth). Render funcD seed7's cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:06:49 -04:00
didericis 192d97a31d Regenerate funcD seed7 figures with the apex-cut model
Reflects cutting up-tooth apexes (except entry teeth): seed7 removes 17
source-dual edges, so its dual retains cycles (not a tree).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:52:28 -04:00
didericis 4e92dde36e Cut up-tooth apexes (except entry teeth) in the dual-cut experiment
Duplicate the apex medial vertex of every singleton up tooth across all
recognised treads -- except each tread's entry tooth, whose apex is left
intact -- in addition to the closing annular-vertex cuts.

For seed59 (source 5) this removes 19 = n-1 source-dual edges and the
remaining dual is a tree (verified for every source/entry choice). The
tree property holds exactly when n-1 distinct edges are cut; some graphs
(e.g. seed7, cutting 17) fall short and retain cycles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:50:56 -04:00
didericis 0a3d7b2615 Draw each full medial tire cut from the dual-cut experiment
Add draw_tire_cuts_png (and a --tire-png flag): one panel per recognised
tread showing the annular cycle, up/down/bite teeth, walk-depth labels, and
cut slits, ported from medial_tire_cut_labelling.to_tikz. Render the
function-D (seed 7) graph's tire cuts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:37:42 -04:00
didericis 367b5adc71 Add source-dual cut experiment with chained entry points
Reads the chained medial tire cut off as a source-dual cut (planar dual of
G with the cut edges removed), as in seed59_min5_dual_cut_1.png, and counts
the missing dual edges around each dual face (vertex of G).

Four chained entry points, broad to narrow control:
  - random_dual_cut: random min-degree-5 maximal planar graph -> ...
  - dual_cut_random_source: random level source -> ...
  - dual_cut_random_entry: random root entry tooth -> ...
  - medial_tire_dual_cut: worker chaining the walk-depth labelling/cut.

Refactor _label_treads to accept an optional root_entry_edge (default
preserves the arbitrary-up-tooth behaviour) so the worker can pin the entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:33:15 -04:00
didericis 94d59ceaed Relabel medial comparison plots by walk depth 2026-06-15 02:11:27 -04:00
didericis 24af5485d2 Add medial cut comparison plots 2026-06-15 01:47:23 -04:00
didericis ea1ab0b986 Add source cap cut to medial tire figures 2026-06-15 01:11:04 -04:00
didericis c64c720e5a Draw the whole medial graph with all tire cuts
Add a --whole mode to draw_medial_tire_cut.py that renders the entire
medial graph M(G) (the assembled cut graph), on a Kamada-Kawai layout,
with the recognised tires highlighted (black annular vertices, blue/red
teeth carrying walk depths, larger red bite apex) and the rest of M(G)
in grey. Add the resulting figure (Figure 3) and a describing paragraph
to the paper for the n=20 seed-72 example, via an \input-ed .tikz file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:07:00 -04:00
didericis 9d7cb7644e Draw a medial tire cut from a random n=20 graph
Add experiments/draw_medial_tire_cut.py, the paper-graphics companion
that imports run_experiment and emits a TikZ panel (walk-depth labels +
cut slits) per recognised tread via to_tikz. Add the resulting figure
(Example 3.2, Figure 2): the single recognised tread T_2 of the medial
tire decomposition of a random maximal planar graph on 20 vertices
(seed 72), an 8-cycle piece with a bite, labelled and cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:55:00 -04:00
didericis a22ca4b888 Add medial tire cut experiment and chaining section
New experiments/run_medial_tire_cut_experiment.py: generates a random
maximal planar graph (stacked seed + random diagonal flips), builds the
medial graph, takes the tire decomposition at a random vertex level
source, walk-depth labels and cuts each full medial tire graph chained
down the tire tree, and assembles one final cut graph of M(G) with a
global label map (data only; graphics go in a separate script).

Fix label_and_cut: the root face is None, which collided with the
next(..., None) sentinel, leaving teeth unlabelled when the entry up
tooth lay inside a bite gap; use a distinct sentinel so the ascent to
the root face runs.

Add a "Chaining across the tire tree" section to the paper, clarifying
that the candidate parent down teeth are the boundary (singleton) down
teeth only -- bite teeth are interior to the parent and shared with no
child, so a lower-walk bite is skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:46:49 -04:00
didericis b4ddc7da8b Add walk-depth labelling/cut script and worked example
New experiments/medial_tire_cut_labelling.py: takes a full medial tire
graph and an entry up tooth and runs the walk-depth labelling-and-cut
procedure, reusing the full medial tire generator's model and emitting
TikZ. Add a generator-produced 8-tooth example to the paper (Figure 1,
Example 2.3) showing the labelling and the two cuts, plus a remark
fixing the cut's closing tooth for descended faces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:00:52 -04:00
didericis 291f7e98c7 Add Medial Tire Cuts paper with walk-depth labelling and cut
New paper "Medial Tire Cuts" citing the medial tire decompositions
paper. States the goal of decomposing the medial graph into a tree of
3-faces, and gives the walk-depth labelling-and-cut procedure for a
single full medial tire graph: a cut duplicates the annular vertex
where a face's tooth traversal closes (planar unzip), reducing the
inner faces to teeth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:39:03 -04:00
didericis b2439e4bac Walkthrough: show only the medial graph in panels C and D
Drop the faint base-graph (G') edges and the dotted restored base edge from the
medial panels, leaving just the medial graph (medial vertices at edge midpoints,
medial edges, colours, halos, and the restored-diagonal medial square). Panels A
and B still show the triangulation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:20:25 -04:00
didericis 20e2cc94b4 Fix walkthrough figure to use verified planar embeddings
The walkthrough previously used a concentric layout whose outer-triangle->ring
spokes can cross -- not a valid plane embedding. Rebuild draw_walkthrough.py on
networkx planar_layout with an explicit crossing check: G, G', and the medial
M(G') drawn at edge midpoints are each verified crossing-free before rendering.
G' is embedded once and reused for panels B/C/D; G reuses it when still planar.

The medial-at-midpoints drawing is planar except for the medial triangle of the
geometric outer face (its midpoint-chords would cut across the unbounded region),
so those three edges are detected via the convex hull and omitted; the remainder
is verified crossing-free. Note updated to describe the embedding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:16:03 -04:00
didericis e7e8536559 Draw the minimal failing graph with a verified planar embedding
draw_failing_graph.py renders seed2 #26 (ring [3,6,3]+face leaf, 12 vertices),
the smallest graph the programme fails on after exhausting sites x tread-phases x
root colour-orders. Uses networkx planar_layout for a straight-line embedding and
verifies no two non-incident edges cross before drawing. Panel A: plain embedding;
panel B: BFS levels with the odd level-2 seam (the inner triangle 9-11-10) bold,
the terminal leaf face shaded -- the face-leaf/gadget spot where removal fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:09:42 -04:00
didericis faf9e01139 Enumerate colour/tread phase over the residue graphs
residue_phase_sweep.py exhaustively enumerates the two colouring control knobs
-- the per-annulus tread phase {0,1}^A and the root-DFS colour order perms(0,1,2)
-- on top of every insertion-site combo, for the graphs the random-phase site
sweep still fails. canonical_coloring_explicit makes this deterministic.

Result (residue_phase_sweep_results.txt): the two hub graphs are RESCUED once
phase is enumerated rather than sampled (so the random-phase fail count overstates
difficulty); the genuine obstructions that survive sites x phases x colour-orders
are exactly the face-leaf graphs (terminal-triangle leaf gadget). Smallest is
seed2 #26 [3,6,3] face (1 combo, 24 settings, all fail at gadget-removal) -- a
minimal obstruction target. Caveat: try_establish is a bounded local Kempe search,
so STILL FAILS means unreachable by the bounded search from canonical-even over
all knob settings, not that no Kempe path exists.

Findings note updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:03:49 -04:00
didericis 9d296eb9c8 Add worked walkthrough; factor explicit phase/colorder colouring
Refactor canonical_coloring into coloring_skeleton (phase-independent parts) +
canonical_coloring_explicit (explicit phases + DFS colour order) + a random-phase
wrapper for back-compat. This exposes the two control knobs deterministically so
they can be enumerated rather than only sampled.

Add a fully worked example on the smallest clean graph (ring [3,5]+hub, 9
vertices, one odd seam, no gadgets): even_program_walkthrough.md traces all six
stages -- generate G with embedding, pick source + BFS levels, choose the diamond
site that evens the level-5 seam, build M(G'), the canonical colouring (seam
mono-3, hub annulus alternates, root by DFS), and a real {1,2}-Kempe switch that
makes the diamond quad reducible. dump_walkthrough.py reproduces every number;
draw_walkthrough.py renders the 4-panel figure even_program_walkthrough.png.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:44:29 -04:00
didericis 2ff712b994 Sweep all diamond insertion sites; report first-match vs full sweep
run_graph no longer takes the first admissible seam edge per odd seam. It now
enumerates every valid diamond site per odd seam (_candidate_sites), sweeps the
full Cartesian product (capped by --max-combos), runs <=4 colour phases per
combination, and counts a graph ok iff SOME placement fully descends. Reports
both the old first-match tally and the swept tally, plus design-space stats and
how many graphs the sweep rescued.

Finding: most "fail:diamond-switch" cases were heuristic, not intrinsic. The
old 39/60 was the first-match heuristic (one point in the design space, and
seed-sensitive 31-39). Sweeping insertion sites rescues ~20 of ~24 first-match
failures:

  seed 1:  first-match 31 ok / 29 fail  ->  sweep 54 ok / 6 fail  (rescued 23)
  seed 2:  first-match 36 ok / 24 fail  ->  sweep 57 ok / 3 fail  (rescued 21)

Only ~3-6 fail:diamond-switch survive the full site sweep -- those are the real
obstruction targets for the joint {1,3}-cycle bipartiteness solver. The colour/
tread phase is still only randomized over 4 attempts, not enumerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:08:17 -04:00
didericis c6e2c3e1a5 Add even-level-cycle colouring program harness
Constructive route: surger G so every level cycle is even (two-vertex leaf gadget
on terminal triangles -> 4-wheel, no defect; diamond on odd internal seams), take
the canonical even colouring of M(G') (no 4CT used), Kempe-remove the planted
degree-4/3 vertices to reach a proper 3-colouring of M(G).

Pipeline runs end to end on synthetic ring triangulations: surgery, canonical
colouring, and gadget removal all work; the program lands on the CYCLE LAYER
(39/60 ok, rest fail:diamond-switch). Diagnostic: a descendable colouring always
EXISTS (M(G) is 3-colourable), so failures are Kempe-reachability from the
canonical even colouring, not non-existence -- the entire difficulty is localised
there. Greedy per-diamond switching is insufficient because diamonds share vertical
{1,3}-Kempe cycles; the principled solve is joint (bipartiteness of the diamond /
side-cycle constraint graph), which is the identified next step. Includes the leaf
gadget figure and a findings note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:36:15 -04:00
didericis d547076cba Verify chain-pigeonhole exhaustively for n<=14 via R_T composition fixpoint
Add kempe_rt_composition_probe.py: Ext(T) = boundary necklaces realisable on a
subtree's outer seam by a compatible Kempe-balanced selection; monotone maps over
minimal-antichain families decide whether empty Ext is reachable. Modeling facts
established: the seam is exactly the singleton down apexes (bite apexes have parent
faces on both sides, hence parent-internal); necklace states are exact because a
child attaches with free dihedral placement (dihedral-closed sequence sets).

Result over all no-length-3-boundary tiles n<=14 (7750 tiles, 1966 distinct
relations, 149 leaf, 27 branching): empty Ext is NOT reachable — every assemblable
tree admits a compatible selection, verifying the chain-pigeonhole conjecture
exhaustively for tire trees with treads n<=14 and no separating triangles. The
fixpoint saturates in 2 rounds: restriction does not accumulate along chains.
Tightest subtree pins a size-5 seam to the single necklace 00012; every smallest
minimal Ext contains the blocky/regular state. Relations cached (~6MB) for cheap
extension to larger n. Caveat: terminal facial-triangle leaves not yet modeled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 02:34:27 -04:00
didericis 28d3d55b92 Refute the regular uniform seam family at n=15
Add kempe_regular_family_test.py (fixed family, per-tile early-exit, --branch-only).
Threads 614/614 at n=12 but FAILS at n=15 on two classes of no-separating-triangle
tiles: non-branching large-even-outer + odd inner (UUUUUUDUDUDUDUD, p=10, face 5) and
branching odd-outer + two even inner faces (UDUDUDDUDUDDDDD bite=(5,12), p=5, [4,4];
11/1022 branching fail). This is the R_T coupling (not a product) biting at scale: the
uniform family sets outer/inner states independently per size. The shortcut was
stronger than the chain-pigeonhole conjecture (which allows per-interface freedom), so
its failure costs a constructive route, not the conjecture; pairwise overlap still
holds. Next line: per-interface R_T composition respecting coupling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:57:13 -04:00
didericis 8b47af6036 n=14 branching case feasible with one regular uniform seam family
Full uniform-family CSP at n=14 --no-tri (4403 tiles, 193 branching) is FEASIBLE:
one family threads every tile incl. branching nodes (outer rim + both inner faces
at once). Independent candidate test threads 193/193 branching tiles. Witness is
fully regular: sigma_m = 0^m if m even (monochromatic), 0^(m-2)12 if m odd. So on
the 4CT-relevant class the chained pigeonhole is constructively resolved throughout
the tested range (n=9,12,14, incl. branching).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:54:48 -04:00
didericis 2b016bc1ca Find smallest n admitting branching tiles (n=11 unrestricted, n=14 no-tri)
Add kempe_branching_min_probe.py (structural: >=2 inner faces with singletons).
Unrestricted branching first appears at n=11; no-separating-triangle branching
(>=2 inner faces each >=4 singletons, p>=4) first appears at n=14 (193 tiles).
Smallest example: word=UUUUDDDDDDDDDD bite=(8,13), p=4, faces root{4,5,6,7} and
bite{9,10,11,12}. n=14 is the smallest place to test the uniform family / R_T
composition on a genuine branching no-separating-triangle tile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:31:19 -04:00
didericis bacbdaaf26 No-separating-triangle restriction removes the chained-seam obstruction
Add --no-tri filter (exclude tiles with a length-3 boundary = separating/non-facial
triangle in G: outer rim of 3 up teeth, or an inner face of exactly 3 singleton
downs) to the trend and uniform-family probes.

The n=12 breaker UUUDUDUDUDUD bite=(3,11) has a size-3 inner face (encloses d5,d7,d9)
and is excluded. With the restriction the size-7 universal at n=12 is restored
(|D[7]| 0->2), every |D[m]|>=1 across n=6..13, and the uniform-family CSP becomes
FEASIBLE at n=12 with the simplest family (monochromatic on even sizes, min-cut on
odd). So the only universal failure was an artifact of admitting non-4-connected
configs; on the 4CT-relevant class gluing is constructively trivial in range.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:21:26 -04:00
didericis 1aa76a5226 Plot the n=12 size-7 universal breaker tile
Add plot_breaker_tile.py and figure for word=UUUDUDUDUDUD bite=(3,11): structure
(7 up teeth = size-7 outer rim, bite (3,11), singleton downs d5,d7,d9) plus a
Kempe-balanced colouring. Reconfirms the outer rim realises 9/10 admissible size-7
necklaces, never 0001112 -- the lone tile that empties the size-7 universal at n=12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:51:36 -04:00
didericis a724a50344 Track odd-size universal trend; the n=12 failure is sporadic, not a trend
Add kempe_universal_trend_probe.py (|D[m]| per size across n). Across n=6..13 and
all sizes, the ONLY empty per-size universal is (n=12, m=7): at n=13 size 7 is back
to |D|=2 with more boundaries (579), so the vanishing is sporadic, not monotone.
The lone n=12 breaker is the outer rim of word=UUUDUDUDUDUD bite=(3,11) (most-
alternating 7-up word, antipodal bite), realising 9/10 size-7 necklaces and missing
only 0001112. Correct the earlier "doomed at scale" reading in the findings note:
the uniform shortcut almost always works (near-total coverage) but is fragile to a
single exceptional tile; pairwise gluability still always holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:40:48 -04:00
didericis b1100b41d9 Add chained-seam findings note (medial pigeonhole)
Write up the R_T coupling, the uniform-family result (feasible n=9, infeasible
n=12 via empty size-7 universal, 0001112 blocked 210/211), and the open threads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:22:27 -04:00
didericis b656b6aed3 Add transfer-relation & uniform-family probes (chained-seam / pigeonhole)
Pursue the paper's medial pigeonhole programme (R_T restriction relation,
chain-pigeonhole conjecture) at the data level.

Findings: R_T (outer<->inner boundary necklace, one Kempe-balanced colouring)
is genuinely coupled, not a product of its projections. A uniform per-size
boundary-state family threading every tile EXISTS at n=9 (unique per size, the
balanced-block necklaces 0011/000011/012/00012 -- not monochromatic), but FAILS
at n=12: size-7 seams admit no universal state (|D[7]|=0; near-universal 0001112
realised on 210/211 boundaries, blocked by one tile). So the uniform "same state
everywhere" shortcut breaks once large odd seams appear and universals vanish as
the tile population grows; the per-interface pigeonhole choice is genuinely
needed. Pairwise gluability still holds, so this locates the conjecture's
difficulty rather than obstructing gluing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:16:27 -04:00
didericis aecbc5ed28 Add tile-overlap probe: per-tile interface subsets always glue
Each tile realises only a subset of the parity-admissible alphabet on its rim,
and tiles genuinely omit interfaces (n=12 m=8: max 273/274, min 43). But any
two tiles always glue: interface subsets always overlap (n=9 m=3-6, n=12 m=3-8)
-- usually via a global universal seam present on every inner+outer rim, and
where none exists (n=12 m=7) the worst pair still shares 14 seams. The universal
seams are the low-complexity ones (<=2 colours, single contiguous block). No
local gluing obstruction; any obstruction must be global across a nested stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:52:38 -04:00
didericis c56da7bb23 Add interface-admissibility probe; confirm parity characterization at n=12
For each interface size m, compare the realized census vocabulary (outer
up-tooth apexes and inner singleton-down apexes) against the full
parity-admissible set. At n=12, m=3..8 every parity-admissible sequence is
realized on both faces (counts 1,4,10,31,91,274; none missing), and up==down
throughout -- the n=9 result is n-independent and scales to m=8. Validated
against the known n=9 answer before running n=12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:47:46 -04:00
didericis d094a310d8 Read up/down apex sequences off the un-deduped census
The anchored single-representative reading interacted with dihedral graph
dedup to record an arbitrary orientation of each necklace, producing a
spurious up-vs-down split at n=9,m=6 (001212 only up, 010122 only down --
the same necklace). Add dihedral_reading_sequences(), which unions the
canonical reading over all 2n dihedral anchors and exactly reproduces the
brute un-deduped census; make it the default for both experiments, with
--anchored to recover the old behaviour. Document the artifact and fix in
kempe_sequence_orientation_note.md.

Regenerate up + down for n=9, m=3..6. Up and down now agree on sequences
and groupings at every m (m=6: identical 31 sequences, 6 groups; the
001212/010122 pair appears on both sides). Groupings coarsen vs anchored
(m=4: 3 groups; m=5: 2 groups) since the orientation-honest vocabulary
merges previously split sequence-sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:25:44 -04:00
didericis d8b5975f81 Add inner-face down-apex colour-sequence experiment (n=9 sweep)
Mirror of the up-tooth experiment with the distinguished valid face moved
from the outer face to an inner non-tooth face (root or bite inner-gap).
For each (M(T), inner face) config holding m singleton down-tooth apexes,
record the apex colour sequence (cyclic order, mod colour permutation) over
Kempe-balanced colourings and group configs by their sequence-set. Runs for
m=3,4,5,6 with per-sequence notes, figures, and a config atlas.

Finding: inner faces realise the same parity-admissible sequence vocabulary
and the same distinct-sequence counts (1/4/10/28) as the outer face, i.e.
the Kempe-parity law acts uniformly on every valid face. At m=6 the configs
are the U<->D embedding mirror of the up-m=6 graphs (matching 7 configs,
28 sequences, 127 colourings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:19:35 -04:00
didericis d93e8d137a Add up-tooth apex colour-sequence experiment over n=9 up-teeth sweep
Enumerate Kempe-balanced 3-colourings of every M(T) with |A(T)|=9 and a
fixed number m of up teeth, record the up-tooth apex colour sequence
(cyclic order, mod colour permutation only), and group the M(T) by their
set of unique sequences. Runs for m=3,4,5,6 with per-sequence notes and
figures plus a summary atlas.

Finding: realised sequences obey outer-face Kempe parity (all three
colour-counts share m's parity). Distinct sequences grow 1/4/10/28 while
M(T) count falls 23/29/18/7 across m=3..6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:04:59 -04:00
didericis a4b3a6fb50 Draw per-graph Realized/Unrealized/Invalid colouring notes
Add draw_tire_realization.py: for each full medial tire graph from the seed-1
analysis, draw every proper 3-colouring (mod colour permutation) in a grid,
each panel coloured by its three colour classes and banner-labelled Realized /
Unrealized / Invalid, and write one standalone note per graph (plus a README
index).  Refactor tire_realization_analysis to expose iter_pieces() yielding
per-piece coloured colourings.

Output: tire_realization_seed1/ with 17 piece notes + figures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:58:41 -04:00
didericis dacef25cbb Add Realized/Unrealized/Invalid tire-colouring analysis
For a random 12-vertex maximal planar graph (sphere convex hull), enumerate
all proper 3-colourings of M(G), take the BFS-level (tire-tree) decomposition
from every source vertex, and build each full medial tire graph M(T) in the
ambient tread-face model (cycle + teeth + bites).  Recognise each M(T) as a
FullMedialTireGraph and label every proper 3-colouring Realized (Kempe-balanced
and a restriction of a global colouring), Unrealized (balanced but not a
restriction), or Invalid (not balanced).

Findings on seed 1 (17 pieces, M(G) with 90 colourings): zero realized-but-
invalid colourings (confirms Remark 5.8 on a real triangulation), and 12 of 17
pieces carry Unrealized colourings -- Kempe-balance is necessary but not
sufficient for realization; it is sufficient only on cap-like all-up/shallow
treads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:36:34 -04:00
didericis cf035243f6 Verify Remark 5.8 on genuine bite treads
Bites arise when the inner outerplanar graph O has a bridge: the bridge
edge is traversed twice by the outer-face walk, so its medial vertex is
adjacent to four annular vertices.

- check_remark58_bite.py: a minimal bite tread (outer 4-cycle + interior
  bridge u-w) restricts to Kempe-balanced on all colourings (outer face).
- check_remark58_bite_rich.py: O = triangle abc + pendant bridge a-d gives
  one bite plus three singleton down teeth in the bite's inner-gap face;
  every restriction is Kempe-balanced (the three gap singletons are a
  rainbow in every global colouring).

Update Remark 5.8's verification note: the bite case, including singletons
in the bite-gap face, is now confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:46:53 -04:00
didericis 5bed8b4dfb Verify Remark 5.8 mechanism; correct it to level-cycle conservation
Computational checks of the necessity of Kempe-balance (Remark 5.8):

- check_medial_face_parity.py shows the naive "even P-coloured vertices
  per medial face" claim is false (odd vertex-faces on the octahedron and
  stacked triangulations), so the original face-parity justification was
  wrong.
- check_remark58_bitefree.py builds genuine bite-free tire pieces (capped
  triangulated annuli) and confirms every proper 3-colouring of M(G)
  restricts to a Kempe-balanced colouring (|A(T)|=6,8,10,12, all
  colourings, zero failures).

Rewrite Remark 5.8 to cite the correct mechanism: the up/down apexes lie
on level cycles, and a P-Kempe cycle meets each level cycle in an even
number of P-coloured incidences (Lemma 5.6).  Note the bite case is not
yet checked end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:33:00 -04:00
didericis 79cbca8e00 Add Kempe-balanced colouring definition and validity classifier
Define Kempe-balanced colourings of a full medial tire graph (Def 5.7):
for each valid face (outer face or interior non-tooth face of B(T)) and
each colour pair {a,b}, the number of tooth apexes incident to the face
coloured a or b must be even.  Add Remark 5.8 (necessity: a colouring of
M(T) extends to M(G) only if it is Kempe-balanced) and rename Lemma 5.5
to "Kempe chains are cycles".

Add kempe_valid_colorings.py: enumerate all proper 3-colourings of a full
medial tire graph, label each Kempe-balanced/valid or invalid, and plot
them with the offending face's Kempe chains and odd apex set highlighted
on invalid panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:00:10 -04:00
didericis 8cc94fb6b9 Add full medial tire graph generator and n=9 atlas
Name A(T) the "annular cycle" (Thm 3.3, Def 3.4); clarify the bite-face
condition in Remark 3.8 to count down-tooth apexes interior to each face;
add the non-incidence stipulation for bite edges to Def 3.7.

Add an exhaustive generator over |A(T)| enforcing the 3.1-3.9 properties
(tooth word, non-crossing non-incident bites, >=3 up teeth, bite-face
condition), plus a plotting script and the n=9 atlas (81 dihedral classes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 12:23:57 -04:00
didericis 4062e87c61 Add figures, Kempe-cycle section, and restriction experiments
Adds two TikZ figures (boundary-state worst cases and annular cycle
counterexample), a new subsection on Kempe-cycle conservation across
medial tires, and the experiment scripts/findings for the medial tire
restriction search and annular cycle condition check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 01:16:05 -04:00
didericis 20fe6c24ca Add medial tire decomposition paper 2026-06-08 15:34:53 -04:00
482 changed files with 31012 additions and 204 deletions
+1
View File
@@ -16,6 +16,7 @@ All papers are at `papers/<name>/paper.tex`. The current set:
| `iterated_reduction_in_reduced_dual` | An Iterated Reduction in the Reduced Dual |
| `level_resolutions_of_maximal_planar_graphs` | Level Resolutions of Maximal Planar Graphs |
| `level_switching` | Level Switching |
| `medial_tire_decompositions_of_plane_triangulations` | Medial Tire Decompositions of Plane Triangulations |
| `nested_tire_decompositions_of_plane_triangulations` | Nested Tire Decompositions of Plane Triangulations |
| `plane_depth` | Plane Depth |
| `plane_depth_sequencing` | Plane Depth Sequencing |
@@ -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))
+8 -4
View File
@@ -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"
+33 -29
View File
@@ -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)
Binary file not shown.
@@ -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}")
Binary file not shown.

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()
+9 -7
View File
@@ -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"
+13 -1
View File
@@ -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
+38 -36
View File
@@ -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)
Binary file not shown.
@@ -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}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

@@ -0,0 +1,289 @@
"""Draw the walk-depth labelling and cut of a medial tire decomposition.
Paper-graphics companion to ``run_medial_tire_cut_experiment.py``: it imports
``run_experiment`` from there, runs the pipeline on a random maximal planar
graph, and emits TikZ. By default it draws one ``tikzpicture`` (walk-depth
labels + cut slits) per recognised full medial tire graph, using ``to_tikz``
from ``medial_tire_cut_labelling``. With ``--whole`` it instead draws a
two-panel Figure 3 graphic: the source graph with its source highlighted and
the whole medial graph M(G) drawn with every medial vertex at the midpoint of
its source edge and labelled by that source edge, with the full BFS-level chain
shown and the currently computed walk-depth labels and cuts marked.
This script only renders; the experiment itself draws nothing. Run with the
repo venv (networkx): ``.venv/bin/python``.
Examples:
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 59 > panels.tex
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 59 --whole > whole.tex
"""
from __future__ import annotations
import argparse
import math
import os
import sys
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
from tire_realization_analysis import triangular_faces # noqa: E402
def tikz_panels(n: int, seed: int, scale: float = 1.6,
min_degree: int = 5, attempts: int = 1000) -> tuple[dict, list[str]]:
"""Run the experiment and return ``(result, panels)``, one TikZ panel per
recognised tread, each showing that tread's walk-depth labelling and cut."""
result = run_experiment(n=n, seed=seed, min_degree=min_degree, attempts=attempts)
panels = []
for d in sorted(result["results"]):
rec = result["results"][d]
panels.append(to_tikz(rec["g"], depth=rec["depth"], cuts=rec["cuts"],
entry_edge=rec["entry_edge"], scale=scale))
return result, panels
# --------------------------------------------------------------------------- #
# Figure 3: the source graph and midpoint drawing of the whole medial graph.
# --------------------------------------------------------------------------- #
def _source_layout(G: nx.Graph) -> dict[int, tuple[float, float]]:
"""Straight-line planar layout for the source graph, normalised to the unit
box and reused by the medial drawing."""
faces, _ = triangular_faces(G)
outer = list(faces[0])
outer_set = set(outer)
raw = {}
for i, v in enumerate(outer):
angle = math.radians(90.0 - i * 360.0 / len(outer))
raw[v] = np.array([math.cos(angle), math.sin(angle)], dtype=float)
inner = [v for v in sorted(G.nodes()) if v not in outer_set]
if inner:
idx = {v: i for i, v in enumerate(inner)}
n = len(inner)
A = np.zeros((n, n))
bx = np.zeros(n)
by = np.zeros(n)
for i, v in enumerate(inner):
nbrs = list(G.neighbors(v))
A[i, i] = 1.0
for w in nbrs:
if w in idx:
A[i, idx[w]] -= 1.0 / len(nbrs)
else:
bx[i] += raw[w][0] / len(nbrs)
by[i] += raw[w][1] / len(nbrs)
xs = np.linalg.solve(A, bx)
ys = np.linalg.solve(A, by)
for v in inner:
raw[v] = np.array([xs[idx[v]], ys[idx[v]]], dtype=float)
pts = np.array([raw[v] for v in G.nodes()], dtype=float)
center = 0.5 * (pts.max(axis=0) + pts.min(axis=0))
span = float(max(*(pts.max(axis=0) - pts.min(axis=0)), 1.0))
return {
v: tuple((raw[v] - center) / span)
for v in G.nodes()
}
def _edge_midpoint(pos: dict, edge) -> tuple[float, float]:
u, v = edge
ux, uy = pos[u]
vx, vy = pos[v]
return (0.5 * (ux + vx), 0.5 * (uy + vy))
def _edge_label(edge) -> str:
u, v = edge
return f"${u}\\!{{-}}\\!{v}$"
def _source_graph_tikz(result: dict, pos: dict, scale: float) -> str:
G, source = result["G"], result["source"]
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" sedge/.style={black!50, line width=0.35pt},")
A(" sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},")
A(" srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]")
def pt(v):
x, y = pos[v]
return f"({x:.3f},{y:.3f})"
for u, v in sorted(G.edges()):
A(f"\\draw[sedge] {pt(u)}--{pt(v)};")
for v in sorted(G.nodes()):
style = "srcv" if v == source else "sv"
A(f"\\node[{style}] at {pt(v)} {{}};")
sx, sy = pos[source]
A(f"\\node[font=\\scriptsize, text=blue!70!black] at ({sx:.3f},{sy - 0.085:.3f}) {{source {source}}};")
A("\\end{tikzpicture}")
return "\n".join(L)
def _medial_midpoint_tikz(result: dict, pos: dict, scale: float) -> str:
"""Draw M(G) with each medial vertex at the midpoint of its source edge.
Every medial vertex is labelled by its source edge; same-level source edges
show the BFS level-chain tooth layers, and interlevel source edges show the
annular layers. Currently computed tire walk-depth labels and cut labels
are overlaid without moving the medial vertices away from their source
edges."""
G, M = result["G"], result["M"]
levels = nx.single_source_shortest_path_length(G, result["source"])
medial_pos = {edge: _edge_midpoint(pos, edge) for edge in M.nodes()}
apex_roles = {}
apex_walks = {}
for r in result["labels"]:
apex_roles[r["apex"]] = r["role"]
apex_walks.setdefault(r["apex"], []).append(r["walk"])
cut_records = []
cut_number = 1
for c in result.get("cap_cuts", []):
cut_records.append((cut_number, c["medial_vertex"], "cap", c))
cut_number += 1
for d in sorted(result["results"]):
rec = result["results"][d]
g, bij = rec["g"], rec["bij"]
for c in rec["cuts"]:
if c.vertex is None:
continue
cut_records.append((cut_number, bij[f"a{c.vertex}"], d, c))
cut_number += 1
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" base/.style={black!12, line width=0.25pt},")
A(" med/.style={black!38, line width=0.32pt},")
A(" annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},")
A(" levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},")
A(" levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},")
A(" levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},")
A(" knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},")
A(" elbl/.style={font=\\tiny, text=black!70, inner sep=0.2pt},")
A(" dlbl/.style={font=\\tiny\\bfseries, text=black, inner sep=0.5pt},")
A(" cut/.style={red!80!black, line width=1.0pt},")
A(" cutlbl/.style={font=\\tiny, text=red!75!black}]")
def pt_med(edge):
x, y = medial_pos[edge]
return f"({x:.3f},{y:.3f})"
def pt_src(v):
x, y = pos[v]
return f"({x:.3f},{y:.3f})"
for u, v in sorted(result["G"].edges()):
A(f"\\draw[base] {pt_src(u)}--{pt_src(v)};")
for u, v in M.edges():
A(f"\\draw[med] {pt_med(u)}--{pt_med(v)};")
def chain_style(edge):
u, v = edge
lu, lv = levels[u], levels[v]
if lu != lv:
return "annv"
if edge in apex_roles:
return "knownv"
return {1: "levone", 2: "levtwo", 3: "levthree"}.get(lu, "annv")
for mv in sorted(M.nodes()):
A(f"\\node[{chain_style(mv)}] at {pt_med(mv)} {{}};")
for mv in sorted(M.nodes()):
x, y = medial_pos[mv]
A(f"\\node[elbl] at ({x:.3f},{y:.3f}) [yshift=-4.8pt] {{{_edge_label(mv)}}};")
for mv in sorted(apex_walks):
x, y = medial_pos[mv]
label = ",".join(str(w) for w in sorted(apex_walks[mv]))
A(f"\\node[dlbl] at ({x:.3f},{y:.3f}) [yshift=5.0pt] {{{label}}};")
for number, mv, _d, _cut in cut_records:
u, v = mv
ux, uy = pos[u]
vx, vy = pos[v]
mx, my = medial_pos[mv]
ex, ey = vx - ux, vy - uy
length = math.hypot(ex, ey) or 1.0
dx, dy = -0.035 * ey / length, 0.035 * ex / length
A(f"\\draw[cut] ({mx - dx:.3f},{my - dy:.3f})--({mx + dx:.3f},{my + dy:.3f});")
A(f"\\node[cutlbl] at ({mx + 2.4 * dx:.3f},{my + 2.4 * dy:.3f}) {{cut {number}}};")
A("\\end{tikzpicture}")
return "\n".join(L)
def medial_tikz(result: dict, scale: float = 7.0) -> str:
"""Two-panel TikZ for Figure 3: the source graph and the midpoint drawing of
its medial graph with all medial vertices labelled, plus the tire
walk-depth labels and cuts."""
pos = _source_layout(result["G"])
source = _source_graph_tikz(result, pos, scale=0.58 * scale)
medial = _medial_midpoint_tikz(result, pos, scale=scale)
return "\n".join([
"\\begin{tabular}{c}",
source,
"\\\\[-0.25ex]",
"{\\scriptsize source graph $G$}",
"\\\\[1.0ex]",
medial,
"\\\\[-0.25ex]",
"{\\scriptsize medial graph $M(G)$ at edge midpoints}",
"\\end{tabular}",
])
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-n", type=int, default=20)
parser.add_argument("--seed", type=int, default=72)
parser.add_argument("--scale", type=float, default=1.6)
parser.add_argument("--min-degree", type=int, default=5,
help="reject random graphs below this minimum degree")
parser.add_argument("--attempts", type=int, default=1000,
help="number of consecutive seeds to try for --min-degree")
parser.add_argument("--whole", action="store_true",
help="draw the whole medial graph M(G) with all cuts, "
"instead of one panel per tread")
args = parser.parse_args()
if args.whole:
result = run_experiment(n=args.n, seed=args.seed,
min_degree=args.min_degree, attempts=args.attempts)
treads = sorted(result["results"])
print(f"% whole medial graph: n={args.n} seed={args.seed} "
f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} "
f"source={result['source']} recognised treads={treads} "
f"|M(G)|={result['M'].number_of_nodes()}")
print(medial_tikz(result, scale=args.scale if args.scale != 1.6 else 7.0))
return
result, panels = tikz_panels(args.n, args.seed, scale=args.scale,
min_degree=args.min_degree, attempts=args.attempts)
treads = sorted(result["results"])
print(f"% medial tire cut: n={args.n} seed={args.seed} "
f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} "
f"source={result['source']} recognised treads={treads}")
if not panels:
print("% (no recognised full medial tire graphs for this graph)")
for d, panel in zip(treads, panels):
g = result["results"][d]["g"]
print(f"% --- tread {d}: |A(T)|={g.n} word={g.tooth_word} "
f"bites={sorted(g.bites)} ---")
print(panel)
if __name__ == "__main__":
main()
@@ -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)]`
Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

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)]`
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@@ -0,0 +1,17 @@
"""Compatibility wrapper for the medial tire cut labelling script."""
from __future__ import annotations
import os
import sys
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 medial_tire_cut_labelling import main
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,620 @@
"""Medial tire cut experiment.
End-to-end experiment for the *Medial Tire Cuts* paper:
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.
4. Walk-depth label and cut each full medial tire graph, chaining the labels
down the tire tree, and assemble one final cut graph of M(G) with a global
label map.
This script produces *data* (the final cut graph and its labels); it draws
nothing. Anything for the paper (figures) lives in a separate script that
imports ``run_experiment`` from here.
Chaining rule (walk depths across the tire tree).
* The root tread (no recognised parent) is entered at an arbitrary up tooth
with walk depth 0.
* A child tread is entered at the up tooth whose apex is the *same medial
vertex* as the parent's down tooth of lowest walk depth -- a parent down
tooth and the child up tooth glued to it across the shared level cycle are
the same medial vertex of M(G). The entry up tooth's walk depth is that
parent down tooth's depth + 1, and the walk increments locally from there.
* The source cap contributes one additional cut. It is placed at the
counter-clockwise source edge incident to the cap down tooth whose apex is
the root tread's entry up tooth.
Run with the repo venv (networkx + scipy): ``.venv/bin/python``.
"""
from __future__ import annotations
import argparse
import json
import os
import random
import subprocess
import sys
from collections import defaultdict
import networkx as nx
# Reuse the realization pipeline from the medial tire decompositions paper, and
# the walk-depth labelling/cut from this paper's companion script.
_HERE = os.path.dirname(os.path.abspath(__file__))
_MTD = os.path.normpath(os.path.join(
_HERE, "..", "..",
"medial_tire_decompositions_of_plane_triangulations", "experiments"))
_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, 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.
# --------------------------------------------------------------------------- #
def _apex_vertex(g, bij, edge):
"""The medial vertex that is the apex of the tooth on annular ``edge``."""
return bij[g.apex_of_edge(edge)]
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):
"""The source-cap cut determined by the first recognised tread's entry.
If the root tread enters at an up tooth whose apex is the level-1 edge
``xy``, then ``xy`` is a down tooth of the source cap. Cut the
counter-clockwise source edge incident to that down tooth. The returned
record also stores the local neighbour split used to unzip the medial
vertex in the whole medial graph.
"""
if not results:
return []
root_depth = min(results)
rec = results[root_depth]
g, bij = rec["g"], rec["bij"]
x, y = _apex_vertex(g, bij, rec["entry_edge"])
if levels.get(x) != 1 or levels.get(y) != 1:
return []
order = list(emb.neighbors_cw_order(source))
if x not in order or y not in order:
return []
next_cw = {v: order[(i + 1) % len(order)] for i, v in enumerate(order)}
prev_cw = {v: order[(i - 1) % len(order)] for i, v in enumerate(order)}
if next_cw[x] == y:
cut_endpoint, other_endpoint = x, y
elif next_cw[y] == x:
cut_endpoint, other_endpoint = y, x
else:
return []
other_cap_endpoint = prev_cw[cut_endpoint]
mv = ekey(source, cut_endpoint)
return [{
"medial_vertex": mv,
"down_tooth": ekey(cut_endpoint, other_endpoint),
"neighbours_a": [
ekey(source, other_endpoint),
ekey(cut_endpoint, other_endpoint),
],
"neighbours_b": [
ekey(source, other_cap_endpoint),
ekey(cut_endpoint, other_cap_endpoint),
],
}]
# --------------------------------------------------------------------------- #
# Assemble one final cut graph of M(G) with a global label map.
# --------------------------------------------------------------------------- #
def _assemble_cut_graph(M, results, cap_cuts=None):
"""Apply every tread's cuts to M(G).
Each cut duplicates an annular medial vertex, splitting its four incident
medial edges along the slit between the two teeth meeting there: the tooth
on the previous annular edge (with that edge's far annular vertex) goes to
one copy, the tooth on the next annular edge to the other.
Returns ``(H, label_records, warnings)`` where ``H`` is the cut graph (a
networkx graph whose split vertices are keyed ``(medial_vertex, "A"/"B",
tread)``) and ``label_records`` lists every tooth's walk depth.
"""
# Per cut annular vertex: map each original neighbour -> which copy keeps it.
split = {} # medial_vertex -> {neighbour_medial_vertex: copy_node}
warnings = []
for i, c in enumerate(cap_cuts or []):
mv = c["medial_vertex"]
if mv in split:
warnings.append(f"cap cut at {mv} was already cut; skipped")
continue
copy_a = (mv, "A", f"cap{i}")
copy_b = (mv, "B", f"cap{i}")
split[mv] = {
**{v: copy_a for v in c["neighbours_a"]},
**{v: copy_b for v in c["neighbours_b"]},
}
for key in sorted(results):
td = key[0]
g, bij = results[key]["g"], results[key]["bij"]
n = g.n
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 {key} cut twice; "
f"second cut not applied")
continue
e_prev, e_next = (kk - 1) % n, kk
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,
bij[f"a{(kk + 1) % n}"]: copy_b,
_apex_vertex(g, bij, e_next): copy_b,
}
def resolve(node, other):
return split[node][other] if node in split else node
H = nx.Graph()
H.add_nodes_from(v for v in M.nodes() if v not in split)
for v, copies in split.items():
H.add_nodes_from(set(copies.values()))
for u, v in M.edges():
H.add_edge(resolve(u, v), resolve(v, u))
label_records = []
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": td, "comp": key[1], "edge": k, "role": role,
"apex": _apex_vertex(g, bij, k), "walk": depth[k],
})
return H, label_records, warnings
# --------------------------------------------------------------------------- #
# Driver.
# --------------------------------------------------------------------------- #
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_connectivity >= 5:
plantri = os.path.normpath(os.path.join(_HERE, "..", "..", "..",
"plantri", "plantri"))
if os.path.exists(plantri):
data = subprocess.check_output(
[plantri, "-c5", "-g", str(n)],
stderr=subprocess.DEVNULL)
graphs = [
line for line in data.splitlines()
if line and not line.startswith(b">>")
]
if graphs:
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 nx.node_connectivity(G) >= min_connectivity:
return G, sample_seed
raise RuntimeError(
f"no random maximal planar graph on {n} vertices with "
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_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)),
``results`` (dict depth -> labelling/cut record), ``skipped`` (list of
(depth, reason)), ``cut_graph`` (networkx graph), ``labels`` (list of tooth
records), ``warnings``.
"""
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, tread_meta = _build_treads(faces, levels)
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, "tread_meta": tread_meta,
"results": results, "cap_cuts": cap_cuts,
"skipped": skipped,
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
}
# --------------------------------------------------------------------------- #
# Serialization / reporting.
# --------------------------------------------------------------------------- #
def _vname(v) -> str:
"""Stable string name for a medial vertex (an edge key) or a split node."""
if isinstance(v, tuple) and len(v) == 3 and v[1] in ("A", "B"):
mv, side, d = v
return f"{mv[0]}-{mv[1]}#{side}@T{d}"
return f"{v[0]}-{v[1]}"
def to_json(result: dict) -> dict:
res = result["results"]
treads_out = []
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"
else "bite" if door_bite(g, k) is not None else "down"),
"apex": _vname(_apex_vertex(g, bij, k)),
"walk": depth[k],
} for k in range(g.n)]
treads_out.append({
"depth": d, "comp": comp, "n": g.n, "tooth_word": g.tooth_word,
"bites": sorted(list(b) for b in g.bites),
"entry_edge": res[key]["entry_edge"], "start_depth": res[key]["start_depth"],
"teeth": teeth,
"cuts": [{
"annular_index": c.vertex,
"annular_vertex": _vname(bij[f"a{c.vertex}"]),
"last_edge": c.last_tooth, "closing_edge": c.closing_tooth,
} for c in cuts],
})
H = result["cut_graph"]
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(),
"skipped": [[d, why] for d, why in result["skipped"]],
"cap_cuts": [{
"medial_vertex": _vname(c["medial_vertex"]),
"down_tooth": _vname(c["down_tooth"]),
} for c in result["cap_cuts"]],
"treads": treads_out,
"cut_graph": {
"nodes": sorted(_vname(v) for v in H.nodes()),
"edges": sorted([_vname(u), _vname(v)] for u, v in H.edges()),
},
"labels": [{
"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"],
}
def summary(result: dict) -> str:
H, res = result["cut_graph"], result["results"]
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, "
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 tires (depth, component): {sorted(res)}",
f"skipped treads: {result['skipped']}",
]
for key in sorted(res):
d, comp = key
g = res[key]["g"]
ncuts = len(res[key]["cuts"])
lines.append(
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, "
f"{len(result['cap_cuts']) + sum(len(r['cuts']) for r in res.values())} cuts total")
if result["warnings"]:
lines.append("warnings: " + "; ".join(result["warnings"]))
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-n", type=int, default=12, help="number of vertices")
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-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 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_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:
json.dump(to_json(result), fh, indent=2)
print(f"wrote {args.json}")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

+1
View File
@@ -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()
+29
View File
@@ -0,0 +1,29 @@
\relax
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Cutting a full medial tire graph}}{1}{}\protected@file@percent }
\newlabel{def:walk-depth-cut}{{2.1}{1}}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\newlabel{rem:closing-tooth}{{2.2}{2}}
\newlabel{ex:worked-cut}{{2.3}{2}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Chaining across the tire tree}}{2}{}\protected@file@percent }
\citation{bauerfeld-medial-tire}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A full medial tire graph (left) and its walk-depth labelling and cut (right), from Example\nonbreakingspace 2.3\hbox {}. Black vertices are the annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth apexes, red vertices are down-tooth apexes, and the larger red vertex is the shared apex of the bite on annular edges $0$ and $4$. On the right, each tooth carries its walk depth, and the two red slits mark the cuts: \emph {cut\nonbreakingspace 1} duplicates $a_5$ as the root-face traversal closes, and \emph {cut\nonbreakingspace 2} duplicates $a_1$ as the bite's inner-gap face closes. After the cuts the only bounded faces are the eight teeth.}}{3}{}\protected@file@percent }
\newlabel{fig:worked-cut}{{1}{3}}
\newlabel{rem:chaining-candidates}{{3.1}{3}}
\newlabel{ex:real-cut}{{3.2}{4}}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces The recognised tread $T_2$ of the medial tire decomposition of a random maximal planar graph on $20$ vertices (Example\nonbreakingspace 3.2\hbox {}), with its walk-depth labelling and cut. Black vertices are the annular medial vertices of $A(T)$; blue vertices are up-tooth apexes and red vertices down-tooth apexes, the larger red vertex being the shared apex of the bite on annular edges $2$ and $5$. Each tooth carries its walk depth; the red slits are the two cuts.}}{4}{}\protected@file@percent }
\newlabel{fig:real-cut}{{2}{4}}
\bibcite{bauerfeld-medial-tire}{1}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces The source graph $G$ and the whole medial graph $M(G)$ of the minimum-degree-$5$ maximal planar graph on $20$ vertices generated by \texttt {plantri -m5} at seed $59$. The source vertex $5$ is highlighted in the top panel. In the bottom panel, each medial vertex is placed at the midpoint of its corresponding source edge and labelled by that edge. Black vertices come from source edges between consecutive levels; coloured vertices come from source edges within a single level of the chain. The red-highlighted vertices, walk-depth labels, and seven red slits are the computed source-cap cut and full-medial-tire labelling cuts for the recognised treads $T_1$ and $T_2$. Drawn by \texttt {experiments/draw\_medial\_tire\_cut.py} with \texttt {--whole --min-degree 5}.}}{5}{}\protected@file@percent }
\newlabel{fig:whole-medial}{{3}{5}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{5}{}\protected@file@percent }
\gdef \@abspage@last{5}
+533
View File
@@ -0,0 +1,533 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 15 JUN 2026 01:07
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
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.te
x
\pgfutil@everybye=\toks28
\pgfutil@tempdima=\dimen152
\pgfutil@tempdimb=\dimen153
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common-li
sts.tex))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def
\pgfutil@abb=\box53
) (/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/pgf.revision.tex)
Package: pgfrcs 2021/05/15 v3.1.9a (3.1.9a)
))
Package: pgf 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
Package: pgfsys 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
\pgfkeys@pathtoks=\toks29
\pgfkeys@temptoks=\toks30
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeysfiltered.c
ode.tex
\pgfkeys@tmptoks=\toks31
))
\pgf@x=\dimen154
\pgf@y=\dimen155
\pgf@xa=\dimen156
\pgf@ya=\dimen157
\pgf@xb=\dimen158
\pgf@yb=\dimen159
\pgf@xc=\dimen160
\pgf@yc=\dimen161
\pgf@xd=\dimen162
\pgf@yd=\dimen163
\w@pgf@writea=\write3
\r@pgf@reada=\read2
\c@pgf@counta=\count272
\c@pgf@countb=\count273
\c@pgf@countc=\count274
\c@pgf@countd=\count275
\t@pgf@toka=\toks32
\t@pgf@tokb=\toks33
\t@pgf@tokc=\toks34
\pgf@sys@id@count=\count276
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg
File: pgf.cfg 2021/05/15 v3.1.9a (3.1.9a)
)
Driver file for pgf: pgfsys-pdftex.def
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.d
ef
File: pgfsys-pdftex.def 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-p
df.def
File: pgfsys-common-pdf.def 2021/05/15 v3.1.9a (3.1.9a)
)))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.
code.tex
File: pgfsyssoftpath.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfsyssoftpath@smallbuffer@items=\count277
\pgfsyssoftpath@bigbuffer@items=\count278
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.
code.tex
File: pgfsysprotocol.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)) (/usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
Package: xcolor 2021/10/31 v2.13 LaTeX color extensions (UK)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
File: color.cfg 2016/01/02 v1.6 sample color configuration
)
Package xcolor Info: Driver file: pdftex.def on input line 227.
Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1352.
Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1356.
Package xcolor Info: Model `RGB' extended on input line 1368.
Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1370.
Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1371.
Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1372.
Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1373.
Package xcolor Info: Model `Gray' substituted by `gray' on input line 1374.
Package xcolor Info: Model `wave' substituted by `hsb' on input line 1375.
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
Package: pgfcore 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex
\pgfmath@dimen=\dimen164
\pgfmath@count=\count279
\pgfmath@box=\box54
\pgfmath@toks=\toks35
\pgfmath@stack@operand=\toks36
\pgfmath@stack@operation=\toks37
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.
tex
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic
.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigo
nometric.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.rando
m.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.compa
rison.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.
code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round
.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.
code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integ
erarithmetics.code.tex)))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex
\c@pgfmathroundto@lastzeros=\count280
)) (/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfint.code.tex)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.co
de.tex
File: pgfcorepoints.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@picminx=\dimen165
\pgf@picmaxx=\dimen166
\pgf@picminy=\dimen167
\pgf@picmaxy=\dimen168
\pgf@pathminx=\dimen169
\pgf@pathmaxx=\dimen170
\pgf@pathminy=\dimen171
\pgf@pathmaxy=\dimen172
\pgf@xx=\dimen173
\pgf@xy=\dimen174
\pgf@yx=\dimen175
\pgf@yy=\dimen176
\pgf@zx=\dimen177
\pgf@zy=\dimen178
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconst
ruct.code.tex
File: pgfcorepathconstruct.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@path@lastx=\dimen179
\pgf@path@lasty=\dimen180
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage
.code.tex
File: pgfcorepathusage.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@shorten@end@additional=\dimen181
\pgf@shorten@start@additional=\dimen182
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.co
de.tex
File: pgfcorescopes.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfpic=\box55
\pgf@hbox=\box56
\pgf@layerbox@main=\box57
\pgf@picture@serial@count=\count281
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicst
ate.code.tex
File: pgfcoregraphicstate.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgflinewidth=\dimen183
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransform
ations.code.tex
File: pgfcoretransformations.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@pt@x=\dimen184
\pgf@pt@y=\dimen185
\pgf@pt@temp=\dimen186
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.cod
e.tex
File: pgfcorequick.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.c
ode.tex
File: pgfcoreobjects.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathproce
ssing.code.tex
File: pgfcorepathprocessing.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.co
de.tex
File: pgfcorearrows.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfarrowsep=\dimen187
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.cod
e.tex
File: pgfcoreshade.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@max=\dimen188
\pgf@sys@shading@range@num=\count282
\pgf@shadingcount=\count283
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.cod
e.tex
File: pgfcoreimage.code.tex 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.
code.tex
File: pgfcoreexternal.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfexternal@startupbox=\box58
))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.co
de.tex
File: pgfcorelayers.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretranspare
ncy.code.tex
File: pgfcoretransparency.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.
code.tex
File: pgfcorepatterns.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.
tex
File: pgfcorerdf.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.cod
e.tex
File: pgfmoduleshapes.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfnodeparttextbox=\box59
)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.
tex
File: pgfmoduleplot.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version
-0-65.sty
Package: pgfcomp-version-0-65 2021/05/15 v3.1.9a (3.1.9a)
\pgf@nodesepstart=\dimen189
\pgf@nodesepend=\dimen190
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version
-1-18.sty
Package: pgfcomp-version-1-18 2021/05/15 v3.1.9a (3.1.9a)
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex)
) (/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
Package: pgffor 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex)
\pgffor@iter=\dimen191
\pgffor@skip=\dimen192
\pgffor@stack=\toks38
\pgffor@toks=\toks39
))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.cod
e.tex
Package: tikz 2021/05/15 v3.1.9a (3.1.9a)
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothan
dlers.code.tex
File: pgflibraryplothandlers.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@plot@mark@count=\count284
\pgfplotmarksize=\dimen193
)
\tikz@lastx=\dimen194
\tikz@lasty=\dimen195
\tikz@lastxsaved=\dimen196
\tikz@lastysaved=\dimen197
\tikz@lastmovetox=\dimen198
\tikz@lastmovetoy=\dimen256
\tikzleveldistance=\dimen257
\tikzsiblingdistance=\dimen258
\tikz@figbox=\box60
\tikz@figbox@bg=\box61
\tikz@tempbox=\box62
\tikz@tempbox@bg=\box63
\tikztreelevel=\count285
\tikznumberofchildren=\count286
\tikznumberofcurrentchild=\count287
\tikz@fig@count=\count288
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.cod
e.tex
File: pgfmodulematrix.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfmatrixcurrentrow=\count289
\pgfmatrixcurrentcolumn=\count290
\pgf@matrix@numberofcolumns=\count291
)
\tikz@expandcount=\count292
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/librarie
s/tikzlibrarytopaths.code.tex
File: tikzlibrarytopaths.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)))
(/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/librarie
s/tikzlibrarybackgrounds.code.tex
File: tikzlibrarybackgrounds.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@layerbox@background=\box64
\pgf@layerboxsaved@background=\box65
)
\c@theorem=\count293
(/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=\count294
\l__pdf_internal_box=\box66
)
(./paper.aux)
\openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 29.
LaTeX Font Info: ... okay on input line 29.
LaTeX Font Info: Trying to load font information for U+msa on input line 29.
(/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 29.
(/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=\count295
\scratchdimen=\dimen259
\scratchbox=\box67
\nofMPsegments=\count296
\nofMParguments=\count297
\everyMPshowfont=\toks40
\MPscratchCnt=\count298
\MPscratchDim=\dimen260
\MPnumerator=\count299
\makeMPintoPDFobject=\count300
\everyMPtoPDFconversion=\toks41
) (/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}]
LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3] (./whole_medial_seed59_min5.tikz) [4] [5] (./paper.aux) )
Here is how much of TeX's memory you used:
13803 strings out of 478268
275582 string characters out of 5846347
655790 words of memory out of 5000000
31627 multiletter control sequences out of 15000+600000
477661 words of font info for 60 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
84i,15n,89p,907b,804s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/te
xlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/tex
live/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx6.pfb></usr/local/texli
ve/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx7.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/cmmi10.pfb></usr/local/texlive/
2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/20
22/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022
/texmf-dist/fonts/type1/public/amsfonts/cm/cmr6.pfb></usr/local/texlive/2022/te
xmf-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-di
st/fonts/type1/public/amsfonts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dis
t/fonts/type1/public/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist
/fonts/type1/public/amsfonts/cm/cmsy6.pfb></usr/local/texlive/2022/texmf-dist/f
onts/type1/public/amsfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fo
nts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmtt10.pfb>
Output written on paper.pdf (5 pages, 226751 bytes).
PDF statistics:
103 PDF objects out of 1000 (max. 8388607)
63 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)
Binary file not shown.
+450
View File
@@ -0,0 +1,450 @@
%% filename: amsart-template.tex
%% American Mathematical Society
%% AMS-LaTeX v.2 template for use with amsart
%% ====================================================================
\documentclass{amsart}
\usepackage{amssymb}
\usepackage{graphicx}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\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{Medial Tire Cuts}
% author one information
\author{Eric Bauerfeld}
\address{}
\curraddr{}
\email{}
\thanks{}
\subjclass[2010]{Primary }
\keywords{plane graph, triangulation, medial graph, tire graph, Tait coloring, Four Colour Theorem}
\date{}
\dedicatory{}
\begin{abstract}
Starting from the medial tire decomposition of a plane triangulation, we
study the cuts that medial tires make in the full medial graph. We will
show how to use medial tires to decompose the medial graph into a tree of
three faces.
\end{abstract}
\maketitle
\section{Introduction}
This paper builds on the medial tire decomposition
of~\cite{bauerfeld-medial-tire}. For a plane triangulation $G$ with
fixed embedding we use freely the terminology and notation introduced
there: the full medial graph $M(G)$, its decomposition into full medial
tire graphs $\mathsf{M}(T)$ indexed by the treads $T$ of the tire tree
$\mathcal{T}(G,S)$ at a level source $S$, the annular medial cycle
$A(T)$, and the boundary medial vertex sets.
We will show how to use medial tires to decompose the medial graph into
a tree of three faces.
\section{Cutting a full medial tire graph}
We first describe a procedure that simultaneously \emph{labels} and
\emph{cuts} a single full medial tire graph $\mathsf{M}(T)$ so that,
after the cuts, the only faces are the outer face and $3$-faces
(triangles)---the teeth of~\cite{bauerfeld-medial-tire}. The labelling
assigns to each tooth an integer \emph{walk depth}; the cuts break the
cyclic adjacencies of the teeth so that what remains is a tree of
$3$-faces.
By a \emph{cut} we mean the duplication of a single vertex of
$\mathsf{M}(T)$: the vertex is split into two copies and the embedding is
slit open along it (a planar unzip), separating the faces that meet only
at that vertex. A cut therefore reduces the number of bounded faces that
are not teeth.
Throughout we use the teeth, up and down teeth, apexes, bites, the
annular medial cycle $A(T)$, and the auxiliary plane graph $B(T)$
of~\cite{bauerfeld-medial-tire}. Each tooth is a $3$-face of
$\mathsf{M}(T)$, and the inner faces of $B(T)$ (the root face and the
bite inner-gap faces) are the larger faces to be cut into teeth.
\begin{definition}[Walk-depth labelling and cut]
\label{def:walk-depth-cut}
Let $\mathsf{M}(T)$ be a full medial tire graph. Assign walk depths and
cuts as follows.
\begin{enumerate}
\item Pick an arbitrary up tooth, the \emph{entry tooth}. It has walk
depth $d$.
\item Traverse all the teeth that bound the inner face incident to the
entry tooth clockwise until we reach the entry tooth, incrementing the
walk depth by $1$ for each tooth traversed. (The \emph{inner face
incident to the entry tooth} is the inner face of $B(T)$ whose boundary
contains the annular edge of $A(T)$ carrying the entry tooth.)
\item When you reach the last tooth in the face, perform a \emph{cut}
by duplicating the annular vertex at which the traversal closes---the
annular vertex of $A(T)$ shared by the last tooth and the entry tooth.
\item Find the tooth $t$ with the highest walk depth which is a member
of a bite.
\item If $t$ is incident to a face $F$ with unlabelled teeth, traverse
the teeth in $F$ starting from $t$ in the direction of the tooth
incident to $t$ which is unlabelled, and increment the walk depth by
$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
entry tooth, so the cut of step (3) duplicates the annular vertex shared
by the last tooth and the entry tooth. For a face $F$ entered in step
(5), the traversal instead closes upon reaching an already-labelled
tooth: the other tooth of the bite through which $F$ was entered. In
both cases the cut of step (3) duplicates the annular vertex shared by
the last newly labelled tooth and this \emph{closing tooth}. Since both
teeth of a bite are labelled while traversing its parent face, every
descended face closes on such a tooth.
\end{remark}
\begin{example}[A worked walk-depth labelling and cut]
\label{ex:worked-cut}
Figure~\ref{fig:worked-cut} shows a full medial tire graph with annular
cycle of length $8$, generated by the full medial tire generator
of~\cite{bauerfeld-medial-tire}. Its eight teeth are: three up teeth on
the annular edges $5,6,7$ in the root face; one bite pairing the annular
edges $0$ and $4$; and three singleton down teeth on the annular edges
$1,2,3$ lying in that bite's inner-gap face.
Take the up tooth on edge $5$ as the entry tooth, with walk depth $0$.
Its inner face is the root face, bounded by the teeth on edges
$5,6,7,0,4$ in clockwise order. Step (2) labels them
\[
5\mapsto 0,\quad 6\mapsto 1,\quad 7\mapsto 2,\quad
0\mapsto 3,\quad 4\mapsto 4,
\]
and step (3) cuts by duplicating the annular vertex $a_5$ shared by the
last tooth (edge $4$) and the entry tooth (edge $5$). The highest-depth
bite tooth is now the one on edge $4$ (walk depth $4$); it is incident to
the still-unlabelled inner-gap face of the bite $(0,4)$. Entering that
face from edge $4$ toward its unlabelled neighbour, step (5) labels the
three down teeth
\[
3\mapsto 5,\quad 2\mapsto 6,\quad 1\mapsto 7,
\]
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 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]
\centering
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\end{tikzpicture}
\qquad
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\node[dlbl] at (0.177,0.427) {3};
\node[dlbl] at (0.704,0.292) {7};
\node[dlbl] at (0.704,-0.292) {6};
\node[dlbl] at (0.292,-0.704) {5};
\node[dlbl] at (-0.177,-0.427) {4};
\node[dlbl] at (-1.101,-0.456) {0};
\node[dlbl] at (-1.101,0.456) {1};
\node[dlbl] at (-0.456,1.101) {2};
\draw[cut] (-0.594,-0.594)--(-0.820,-0.820);
\node[cutlbl] at (-0.919,-0.919) {cut 1};
\draw[cut] (0.594,0.594)--(0.820,0.820);
\node[cutlbl] at (0.919,0.919) {cut 2};
\node[lbl, text=blue!60!black] at (-1.663,-0.689) {entry};
\end{tikzpicture}
\caption{A full medial tire graph (left) and its walk-depth labelling and
cut (right), from Example~\ref{ex:worked-cut}. Black vertices are the
annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth
apexes, red vertices are down-tooth apexes, and the larger red vertex is
the shared apex of the bite on annular edges $0$ and $4$. On the right,
each tooth carries its walk depth, and the two red slits mark the cuts:
\emph{cut~1} duplicates $a_5$ as the root-face traversal closes, and
\emph{cut~2} duplicates $a_1$ as the bite's inner-gap face closes. After
the cuts the only bounded faces are the eight teeth.}
\label{fig:worked-cut}
\end{figure}
\section{Chaining across the tire tree}
Definition~\ref{def:walk-depth-cut} labels and cuts a single full medial
tire graph. We extend it to the whole medial graph $M(G)$ through the
medial tire decomposition of~\cite{bauerfeld-medial-tire}: the tire tree
decomposes $M(G)$ into full medial tire graphs $\mathsf{M}(T)$, one per
tread $T$, glued along their boundary medial vertices. A parent tread's
inner level cycle is a child tread's outer level cycle, and the boundary
medial vertices on that shared cycle belong to both treads.
The key incidence is this. A \emph{boundary} (singleton) down tooth of a
parent tread and the up tooth of the child tread glued to it across the
shared level cycle have the \emph{same apex}: both apexes are the same
medial vertex of $M(G)$, namely the medial vertex of an edge with both
endpoints on the shared level cycle. We use this to carry the walk depth
from a parent into its children.
We label tread by tread, outward from the root:
\begin{itemize}
\item a tread with no parent in the decomposition---in particular the
innermost recognised tread---is treated as a \emph{root} and entered at
an arbitrary up tooth with walk depth $0$;
\item a child tread is entered at the up tooth whose apex is the parent's
boundary down tooth of lowest walk depth; that entry up tooth's walk depth
is one more than that down tooth's, and the walk then increments locally
within the child as in Definition~\ref{def:walk-depth-cut}.
\end{itemize}
The source cap contributes one additional cut before the recognised
treads are assembled. If the root tread enters at an up tooth whose apex
is the cap down tooth $xy$, we cut the cap annular vertex corresponding
to the counter-clockwise source edge incident to $xy$. In the example of
Figure~\ref{fig:whole-medial}, the root entry apex is the cap down tooth
$14\!-\!4$, so the cap cut is placed at the medial vertex $14\!-\!5$.
\begin{remark}[Candidate down teeth for chaining]
\label{rem:chaining-candidates}
The down teeth eligible to fix a child's entry are exactly the
\emph{boundary} (singleton) down teeth of the parent: those lying in a
single tread face, whose apex is the shared boundary medial vertex glued to
a child up tooth. A bite's two down teeth are \emph{not} eligible. By the
definition of a bite in~\cite{bauerfeld-medial-tire} its annular edge borders
two tread faces, so a bite tooth is interior to the parent tread and its
apex is a boundary medial vertex of no child. Hence ``the down tooth of
lowest walk depth'' is read among the boundary down teeth only; a bite of
even lower walk depth is skipped.
\end{remark}
Applying every tread's cuts to $M(G)$ assembles the per-tread labellings
and cuts into a single cut graph of $M(G)$ together with a global
walk-depth label map. This pipeline---random maximal planar graph, medial
graph, tire decomposition at a vertex level source, and chained walk-depth
labelling and cut---is carried out by the experiment script
\texttt{experiments/run\_medial\_tire\_cut\_experiment.py}.
\begin{example}[A medial tire cut from a random graph]
\label{ex:real-cut}
Run on a random maximal planar graph on $20$ vertices (seed $72$, level
source vertex $9$), the experiment yields a single recognised tread
$T_2$, drawn in Figure~\ref{fig:real-cut} with the walk-depth labelling
and cut emitted by the graphics companion
\texttt{experiments/draw\_medial\_tire\_cut.py}. Its annular cycle has
length $8$, with up teeth on annular edges $0,3,4$, singleton down teeth
on $1,6,7$, and a bite on the non-incident annular edges $2$ and $5$ (the
central shared apex). Entering at the up tooth on edge $0$ with walk
depth $0$, the root face is labelled in order ($0,1,2$ then $3,4,5$) and
\emph{cut~1} duplicates $a_0$ as it closes; the walk then descends through
the bite into its inner-gap face, labelling the two teeth there ($6,7$),
and \emph{cut~2} duplicates $a_3$ as that face closes. The two cuts leave
only the outer face and the eight teeth as $3$-faces.
\end{example}
\begin{figure}[h]
\centering
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (0.559,1.349)--(0.000,1.000) (0.559,1.349)--(0.707,0.707);
\draw[tth] (0.559,-1.349)--(0.707,-0.707) (0.559,-1.349)--(0.000,-1.000);
\draw[tth] (-0.559,-1.349)--(0.000,-1.000) (-0.559,-1.349)--(-0.707,-0.707);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (-0.554,0.230)--(-1.000,-0.000) (-0.554,0.230)--(-0.707,0.707);
\draw[tth] (-0.230,0.554)--(-0.707,0.707) (-0.230,0.554)--(0.000,1.000);
\draw[tth] (0.000,-0.318)--(1.000,0.000) (0.000,-0.318)--(0.707,-0.707);
\draw[tth] (0.000,-0.318)--(-0.707,-0.707) (0.000,-0.318)--(-1.000,-0.000);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (0.559,1.349) {};
\node[upv] at (0.559,-1.349) {};
\node[upv] at (-0.559,-1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (-0.554,0.230) {};
\node[downv] at (-0.230,0.554) {};
\node[bitev] at (0.000,-0.318) {};
\node[dlbl] at (0.456,1.101) {0};
\node[dlbl] at (0.704,0.292) {1};
\node[dlbl] at (0.427,-0.336) {2};
\node[dlbl] at (0.456,-1.101) {7};
\node[dlbl] at (-0.456,-1.101) {6};
\node[dlbl] at (-0.427,-0.336) {3};
\node[dlbl] at (-0.704,0.292) {4};
\node[dlbl] at (-0.292,0.704) {5};
\draw[cut] (0.000,0.840)--(0.000,1.160);
\node[cutlbl] at (0.000,1.300) {cut 1};
\draw[cut] (0.594,-0.594)--(0.820,-0.820);
\node[cutlbl] at (0.919,-0.919) {cut 2};
\node[lbl, text=blue!60!black] at (0.689,1.663) {entry};
\end{tikzpicture}
\caption{The recognised tread $T_2$ of the medial tire decomposition of a
random maximal planar graph on $20$ vertices
(Example~\ref{ex:real-cut}), with its walk-depth labelling and cut. Black
vertices are the annular medial vertices of $A(T)$; blue vertices are
up-tooth apexes and red vertices down-tooth apexes, the larger red vertex
being the shared apex of the bite on annular edges $2$ and $5$. Each
tooth carries its walk depth; the red slits are the two cuts.}
\label{fig:real-cut}
\end{figure}
Figure~\ref{fig:whole-medial} repeats the whole-medial-graph drawing on a
random maximal planar graph on $20$ vertices with minimum degree $5$
(plantri seed $59$, level source vertex $5$). The experiment recognises
two full medial tire treads, $T_1$ and $T_2$, and produces seven cuts:
one source-cap cut and six full-tread cuts. The
top panel shows the source triangulation with its level source
highlighted; the bottom panel draws $M(G)$ on the same straight-line
embedding by placing each medial vertex at the midpoint of its
corresponding source edge. Every medial vertex is labelled by that source
edge. Black vertices correspond to source edges joining consecutive
levels, and coloured vertices correspond to source edges within one level.
The red-highlighted vertices, walk-depth labels, and red slits are the
computed full-medial-tire labelling and cuts.
\begin{figure}[h]
\centering
\input{whole_medial_seed59_min5.tikz}
\caption{The source graph $G$ and the whole medial graph $M(G)$ of the
minimum-degree-$5$ maximal planar graph on $20$ vertices generated by
\texttt{plantri -m5} at seed $59$. The source vertex $5$ is highlighted
in the top panel. In the bottom panel, each medial vertex is placed at
the midpoint of its corresponding source edge and labelled by that edge.
Black vertices come from source edges between consecutive levels; coloured
vertices come from source edges within a single level of the chain. The
red-highlighted vertices, walk-depth labels, and seven red slits are the
computed source-cap cut and full-medial-tire labelling cuts for the
recognised treads $T_1$ and $T_2$. Drawn by
\texttt{experiments/draw\_medial\_tire\_cut.py} with
\texttt{--whole --min-degree 5}.}
\label{fig:whole-medial}
\end{figure}
\begin{thebibliography}{9}
\bibitem{bauerfeld-medial-tire}
E.~Bauerfeld,
\emph{Medial Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\end{thebibliography}
\end{document}
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

@@ -0,0 +1,403 @@
% whole medial graph: n=20 seed=59 graph_seed=59 min_degree=5 source=5 recognised treads=[1, 2] |M(G)|=54
\begin{tabular}{c}
\begin{tikzpicture}[scale=4.06,
sedge/.style={black!50, line width=0.35pt},
sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},
srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]
\draw[sedge] (0.000,0.433)--(-0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.084,-0.096);
\draw[sedge] (0.000,0.433)--(-0.018,0.026);
\draw[sedge] (0.000,0.433)--(-0.128,-0.048);
\draw[sedge] (-0.500,-0.433)--(0.500,-0.433);
\draw[sedge] (-0.500,-0.433)--(-0.128,-0.048);
\draw[sedge] (-0.500,-0.433)--(-0.073,-0.186);
\draw[sedge] (-0.500,-0.433)--(0.019,-0.303);
\draw[sedge] (0.500,-0.433)--(0.084,-0.096);
\draw[sedge] (0.500,-0.433)--(0.019,-0.303);
\draw[sedge] (0.500,-0.433)--(0.139,-0.245);
\draw[sedge] (0.084,-0.096)--(-0.018,0.026);
\draw[sedge] (0.084,-0.096)--(0.139,-0.245);
\draw[sedge] (0.084,-0.096)--(0.060,-0.177);
\draw[sedge] (0.084,-0.096)--(0.035,-0.147);
\draw[sedge] (0.084,-0.096)--(0.024,-0.131);
\draw[sedge] (0.084,-0.096)--(0.015,-0.113);
\draw[sedge] (0.084,-0.096)--(0.002,-0.076);
\draw[sedge] (-0.018,0.026)--(-0.128,-0.048);
\draw[sedge] (-0.018,0.026)--(0.002,-0.076);
\draw[sedge] (-0.018,0.026)--(-0.048,-0.081);
\draw[sedge] (-0.128,-0.048)--(-0.073,-0.186);
\draw[sedge] (-0.128,-0.048)--(-0.048,-0.081);
\draw[sedge] (-0.073,-0.186)--(0.019,-0.303);
\draw[sedge] (-0.073,-0.186)--(-0.048,-0.081);
\draw[sedge] (-0.073,-0.186)--(-0.023,-0.119);
\draw[sedge] (-0.073,-0.186)--(-0.012,-0.141);
\draw[sedge] (-0.073,-0.186)--(-0.003,-0.156);
\draw[sedge] (-0.073,-0.186)--(0.010,-0.177);
\draw[sedge] (-0.073,-0.186)--(0.031,-0.218);
\draw[sedge] (0.019,-0.303)--(0.139,-0.245);
\draw[sedge] (0.019,-0.303)--(0.031,-0.218);
\draw[sedge] (0.139,-0.245)--(0.060,-0.177);
\draw[sedge] (0.139,-0.245)--(0.031,-0.218);
\draw[sedge] (0.060,-0.177)--(0.035,-0.147);
\draw[sedge] (0.060,-0.177)--(0.010,-0.177);
\draw[sedge] (0.060,-0.177)--(0.031,-0.218);
\draw[sedge] (0.035,-0.147)--(0.024,-0.131);
\draw[sedge] (0.035,-0.147)--(-0.003,-0.156);
\draw[sedge] (0.035,-0.147)--(0.010,-0.177);
\draw[sedge] (0.024,-0.131)--(0.015,-0.113);
\draw[sedge] (0.024,-0.131)--(-0.012,-0.141);
\draw[sedge] (0.024,-0.131)--(-0.003,-0.156);
\draw[sedge] (0.015,-0.113)--(0.002,-0.076);
\draw[sedge] (0.015,-0.113)--(-0.023,-0.119);
\draw[sedge] (0.015,-0.113)--(-0.012,-0.141);
\draw[sedge] (0.002,-0.076)--(-0.048,-0.081);
\draw[sedge] (0.002,-0.076)--(-0.023,-0.119);
\draw[sedge] (-0.048,-0.081)--(-0.023,-0.119);
\draw[sedge] (-0.023,-0.119)--(-0.012,-0.141);
\draw[sedge] (-0.012,-0.141)--(-0.003,-0.156);
\draw[sedge] (-0.003,-0.156)--(0.010,-0.177);
\draw[sedge] (0.010,-0.177)--(0.031,-0.218);
\node[sv] at (0.000,0.433) {};
\node[sv] at (-0.500,-0.433) {};
\node[sv] at (0.500,-0.433) {};
\node[sv] at (0.084,-0.096) {};
\node[sv] at (-0.018,0.026) {};
\node[srcv] at (-0.128,-0.048) {};
\node[sv] at (-0.073,-0.186) {};
\node[sv] at (0.019,-0.303) {};
\node[sv] at (0.139,-0.245) {};
\node[sv] at (0.060,-0.177) {};
\node[sv] at (0.035,-0.147) {};
\node[sv] at (0.024,-0.131) {};
\node[sv] at (0.015,-0.113) {};
\node[sv] at (0.002,-0.076) {};
\node[sv] at (-0.048,-0.081) {};
\node[sv] at (-0.023,-0.119) {};
\node[sv] at (-0.012,-0.141) {};
\node[sv] at (-0.003,-0.156) {};
\node[sv] at (0.010,-0.177) {};
\node[sv] at (0.031,-0.218) {};
\node[font=\scriptsize, text=blue!70!black] at (-0.128,-0.133) {source 5};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize source graph $G$}
\\[1.0ex]
\begin{tikzpicture}[scale=7.0,
base/.style={black!12, line width=0.25pt},
med/.style={black!38, line width=0.32pt},
annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},
levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},
levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},
levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},
knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},
elbl/.style={font=\tiny, text=black!70, inner sep=0.2pt},
dlbl/.style={font=\tiny\bfseries, text=black, inner sep=0.5pt},
cut/.style={red!80!black, line width=1.0pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[base] (0.000,0.433)--(-0.500,-0.433);
\draw[base] (0.000,0.433)--(0.500,-0.433);
\draw[base] (0.000,0.433)--(0.084,-0.096);
\draw[base] (0.000,0.433)--(-0.018,0.026);
\draw[base] (0.000,0.433)--(-0.128,-0.048);
\draw[base] (-0.500,-0.433)--(0.500,-0.433);
\draw[base] (-0.500,-0.433)--(-0.128,-0.048);
\draw[base] (-0.500,-0.433)--(-0.073,-0.186);
\draw[base] (-0.500,-0.433)--(0.019,-0.303);
\draw[base] (0.500,-0.433)--(0.084,-0.096);
\draw[base] (0.500,-0.433)--(0.019,-0.303);
\draw[base] (0.500,-0.433)--(0.139,-0.245);
\draw[base] (0.084,-0.096)--(-0.018,0.026);
\draw[base] (0.084,-0.096)--(0.139,-0.245);
\draw[base] (0.084,-0.096)--(0.060,-0.177);
\draw[base] (0.084,-0.096)--(0.035,-0.147);
\draw[base] (0.084,-0.096)--(0.024,-0.131);
\draw[base] (0.084,-0.096)--(0.015,-0.113);
\draw[base] (0.084,-0.096)--(0.002,-0.076);
\draw[base] (-0.018,0.026)--(-0.128,-0.048);
\draw[base] (-0.018,0.026)--(0.002,-0.076);
\draw[base] (-0.018,0.026)--(-0.048,-0.081);
\draw[base] (-0.128,-0.048)--(-0.073,-0.186);
\draw[base] (-0.128,-0.048)--(-0.048,-0.081);
\draw[base] (-0.073,-0.186)--(0.019,-0.303);
\draw[base] (-0.073,-0.186)--(-0.048,-0.081);
\draw[base] (-0.073,-0.186)--(-0.023,-0.119);
\draw[base] (-0.073,-0.186)--(-0.012,-0.141);
\draw[base] (-0.073,-0.186)--(-0.003,-0.156);
\draw[base] (-0.073,-0.186)--(0.010,-0.177);
\draw[base] (-0.073,-0.186)--(0.031,-0.218);
\draw[base] (0.019,-0.303)--(0.139,-0.245);
\draw[base] (0.019,-0.303)--(0.031,-0.218);
\draw[base] (0.139,-0.245)--(0.060,-0.177);
\draw[base] (0.139,-0.245)--(0.031,-0.218);
\draw[base] (0.060,-0.177)--(0.035,-0.147);
\draw[base] (0.060,-0.177)--(0.010,-0.177);
\draw[base] (0.060,-0.177)--(0.031,-0.218);
\draw[base] (0.035,-0.147)--(0.024,-0.131);
\draw[base] (0.035,-0.147)--(-0.003,-0.156);
\draw[base] (0.035,-0.147)--(0.010,-0.177);
\draw[base] (0.024,-0.131)--(0.015,-0.113);
\draw[base] (0.024,-0.131)--(-0.012,-0.141);
\draw[base] (0.024,-0.131)--(-0.003,-0.156);
\draw[base] (0.015,-0.113)--(0.002,-0.076);
\draw[base] (0.015,-0.113)--(-0.023,-0.119);
\draw[base] (0.015,-0.113)--(-0.012,-0.141);
\draw[base] (0.002,-0.076)--(-0.048,-0.081);
\draw[base] (0.002,-0.076)--(-0.023,-0.119);
\draw[base] (-0.048,-0.081)--(-0.023,-0.119);
\draw[base] (-0.023,-0.119)--(-0.012,-0.141);
\draw[base] (-0.012,-0.141)--(-0.003,-0.156);
\draw[base] (-0.003,-0.156)--(0.010,-0.177);
\draw[base] (0.010,-0.177)--(0.031,-0.218);
\draw[med] (-0.250,0.000)--(-0.064,0.192);
\draw[med] (-0.250,0.000)--(0.250,0.000);
\draw[med] (-0.250,0.000)--(0.000,-0.433);
\draw[med] (-0.250,0.000)--(-0.314,-0.241);
\draw[med] (0.250,0.000)--(0.042,0.169);
\draw[med] (0.250,0.000)--(0.000,-0.433);
\draw[med] (0.250,0.000)--(0.292,-0.264);
\draw[med] (0.042,0.169)--(-0.009,0.230);
\draw[med] (0.042,0.169)--(0.292,-0.264);
\draw[med] (0.042,0.169)--(0.033,-0.035);
\draw[med] (-0.009,0.230)--(-0.064,0.192);
\draw[med] (-0.009,0.230)--(0.033,-0.035);
\draw[med] (-0.009,0.230)--(-0.073,-0.011);
\draw[med] (-0.064,0.192)--(-0.073,-0.011);
\draw[med] (-0.064,0.192)--(-0.314,-0.241);
\draw[med] (0.000,-0.433)--(-0.240,-0.368);
\draw[med] (0.000,-0.433)--(0.260,-0.368);
\draw[med] (-0.314,-0.241)--(-0.286,-0.310);
\draw[med] (-0.314,-0.241)--(-0.100,-0.117);
\draw[med] (-0.286,-0.310)--(-0.240,-0.368);
\draw[med] (-0.286,-0.310)--(-0.100,-0.117);
\draw[med] (-0.286,-0.310)--(-0.027,-0.245);
\draw[med] (-0.240,-0.368)--(-0.027,-0.245);
\draw[med] (-0.240,-0.368)--(0.260,-0.368);
\draw[med] (0.292,-0.264)--(0.319,-0.339);
\draw[med] (0.292,-0.264)--(0.111,-0.171);
\draw[med] (0.260,-0.368)--(0.319,-0.339);
\draw[med] (0.260,-0.368)--(0.079,-0.274);
\draw[med] (0.319,-0.339)--(0.079,-0.274);
\draw[med] (0.319,-0.339)--(0.111,-0.171);
\draw[med] (0.033,-0.035)--(0.043,-0.086);
\draw[med] (0.033,-0.035)--(-0.008,-0.025);
\draw[med] (0.111,-0.171)--(0.072,-0.136);
\draw[med] (0.111,-0.171)--(0.099,-0.211);
\draw[med] (0.072,-0.136)--(0.059,-0.122);
\draw[med] (0.072,-0.136)--(0.099,-0.211);
\draw[med] (0.072,-0.136)--(0.047,-0.162);
\draw[med] (0.059,-0.122)--(0.054,-0.113);
\draw[med] (0.059,-0.122)--(0.047,-0.162);
\draw[med] (0.059,-0.122)--(0.029,-0.139);
\draw[med] (0.054,-0.113)--(0.049,-0.104);
\draw[med] (0.054,-0.113)--(0.029,-0.139);
\draw[med] (0.054,-0.113)--(0.019,-0.122);
\draw[med] (0.049,-0.104)--(0.043,-0.086);
\draw[med] (0.049,-0.104)--(0.019,-0.122);
\draw[med] (0.049,-0.104)--(0.008,-0.095);
\draw[med] (0.043,-0.086)--(0.008,-0.095);
\draw[med] (0.043,-0.086)--(-0.008,-0.025);
\draw[med] (-0.073,-0.011)--(-0.033,-0.027);
\draw[med] (-0.073,-0.011)--(-0.088,-0.064);
\draw[med] (-0.008,-0.025)--(-0.033,-0.027);
\draw[med] (-0.008,-0.025)--(-0.023,-0.079);
\draw[med] (-0.033,-0.027)--(-0.023,-0.079);
\draw[med] (-0.033,-0.027)--(-0.088,-0.064);
\draw[med] (-0.100,-0.117)--(-0.088,-0.064);
\draw[med] (-0.100,-0.117)--(-0.060,-0.134);
\draw[med] (-0.088,-0.064)--(-0.060,-0.134);
\draw[med] (-0.027,-0.245)--(-0.021,-0.202);
\draw[med] (-0.027,-0.245)--(0.025,-0.260);
\draw[med] (-0.060,-0.134)--(-0.048,-0.153);
\draw[med] (-0.060,-0.134)--(-0.035,-0.100);
\draw[med] (-0.048,-0.153)--(-0.042,-0.164);
\draw[med] (-0.048,-0.153)--(-0.035,-0.100);
\draw[med] (-0.048,-0.153)--(-0.018,-0.130);
\draw[med] (-0.042,-0.164)--(-0.038,-0.171);
\draw[med] (-0.042,-0.164)--(-0.018,-0.130);
\draw[med] (-0.042,-0.164)--(-0.008,-0.149);
\draw[med] (-0.038,-0.171)--(-0.031,-0.182);
\draw[med] (-0.038,-0.171)--(-0.008,-0.149);
\draw[med] (-0.038,-0.171)--(0.003,-0.167);
\draw[med] (-0.031,-0.182)--(-0.021,-0.202);
\draw[med] (-0.031,-0.182)--(0.003,-0.167);
\draw[med] (-0.031,-0.182)--(0.021,-0.197);
\draw[med] (-0.021,-0.202)--(0.021,-0.197);
\draw[med] (-0.021,-0.202)--(0.025,-0.260);
\draw[med] (0.079,-0.274)--(0.025,-0.260);
\draw[med] (0.079,-0.274)--(0.085,-0.231);
\draw[med] (0.025,-0.260)--(0.085,-0.231);
\draw[med] (0.099,-0.211)--(0.085,-0.231);
\draw[med] (0.099,-0.211)--(0.045,-0.197);
\draw[med] (0.085,-0.231)--(0.045,-0.197);
\draw[med] (0.047,-0.162)--(0.035,-0.177);
\draw[med] (0.047,-0.162)--(0.022,-0.162);
\draw[med] (0.035,-0.177)--(0.045,-0.197);
\draw[med] (0.035,-0.177)--(0.021,-0.197);
\draw[med] (0.035,-0.177)--(0.022,-0.162);
\draw[med] (0.045,-0.197)--(0.021,-0.197);
\draw[med] (0.029,-0.139)--(0.016,-0.152);
\draw[med] (0.029,-0.139)--(0.010,-0.144);
\draw[med] (0.016,-0.152)--(0.022,-0.162);
\draw[med] (0.016,-0.152)--(0.003,-0.167);
\draw[med] (0.016,-0.152)--(0.010,-0.144);
\draw[med] (0.022,-0.162)--(0.003,-0.167);
\draw[med] (0.019,-0.122)--(0.006,-0.136);
\draw[med] (0.019,-0.122)--(0.001,-0.127);
\draw[med] (0.006,-0.136)--(0.010,-0.144);
\draw[med] (0.006,-0.136)--(-0.008,-0.149);
\draw[med] (0.006,-0.136)--(0.001,-0.127);
\draw[med] (0.010,-0.144)--(-0.008,-0.149);
\draw[med] (0.008,-0.095)--(-0.004,-0.116);
\draw[med] (0.008,-0.095)--(-0.011,-0.098);
\draw[med] (-0.004,-0.116)--(0.001,-0.127);
\draw[med] (-0.004,-0.116)--(-0.018,-0.130);
\draw[med] (-0.004,-0.116)--(-0.011,-0.098);
\draw[med] (0.001,-0.127)--(-0.018,-0.130);
\draw[med] (-0.023,-0.079)--(-0.011,-0.098);
\draw[med] (-0.023,-0.079)--(-0.035,-0.100);
\draw[med] (-0.011,-0.098)--(-0.035,-0.100);
\node[knownv] at (-0.250,0.000) {};
\node[annv] at (0.250,0.000) {};
\node[annv] at (0.042,0.169) {};
\node[knownv] at (-0.009,0.230) {};
\node[annv] at (-0.064,0.192) {};
\node[annv] at (0.000,-0.433) {};
\node[annv] at (-0.314,-0.241) {};
\node[knownv] at (-0.286,-0.310) {};
\node[annv] at (-0.240,-0.368) {};
\node[knownv] at (0.292,-0.264) {};
\node[knownv] at (0.260,-0.368) {};
\node[annv] at (0.319,-0.339) {};
\node[annv] at (0.033,-0.035) {};
\node[annv] at (0.111,-0.171) {};
\node[annv] at (0.072,-0.136) {};
\node[annv] at (-0.073,-0.011) {};
\node[annv] at (-0.100,-0.117) {};
\node[annv] at (-0.027,-0.245) {};
\node[annv] at (0.079,-0.274) {};
\node[knownv] at (0.099,-0.211) {};
\node[annv] at (0.059,-0.122) {};
\node[knownv] at (0.047,-0.162) {};
\node[knownv] at (0.029,-0.139) {};
\node[annv] at (0.016,-0.152) {};
\node[annv] at (0.022,-0.162) {};
\node[annv] at (0.054,-0.113) {};
\node[knownv] at (0.019,-0.122) {};
\node[annv] at (0.006,-0.136) {};
\node[annv] at (0.010,-0.144) {};
\node[annv] at (0.049,-0.104) {};
\node[annv] at (0.008,-0.095) {};
\node[annv] at (-0.004,-0.116) {};
\node[annv] at (0.001,-0.127) {};
\node[knownv] at (0.043,-0.086) {};
\node[annv] at (-0.008,-0.025) {};
\node[annv] at (-0.023,-0.079) {};
\node[knownv] at (-0.011,-0.098) {};
\node[knownv] at (-0.033,-0.027) {};
\node[annv] at (-0.088,-0.064) {};
\node[knownv] at (-0.060,-0.134) {};
\node[annv] at (-0.035,-0.100) {};
\node[annv] at (-0.048,-0.153) {};
\node[knownv] at (-0.018,-0.130) {};
\node[annv] at (-0.042,-0.164) {};
\node[knownv] at (-0.008,-0.149) {};
\node[annv] at (-0.038,-0.171) {};
\node[knownv] at (0.003,-0.167) {};
\node[annv] at (-0.031,-0.182) {};
\node[annv] at (0.035,-0.177) {};
\node[knownv] at (0.021,-0.197) {};
\node[annv] at (-0.021,-0.202) {};
\node[knownv] at (0.025,-0.260) {};
\node[annv] at (0.085,-0.231) {};
\node[annv] at (0.045,-0.197) {};
\node[elbl] at (-0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!1$};
\node[elbl] at (0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!2$};
\node[elbl] at (0.042,0.169) [yshift=-4.8pt] {$0\!{-}\!3$};
\node[elbl] at (-0.009,0.230) [yshift=-4.8pt] {$0\!{-}\!4$};
\node[elbl] at (-0.064,0.192) [yshift=-4.8pt] {$0\!{-}\!5$};
\node[elbl] at (0.000,-0.433) [yshift=-4.8pt] {$1\!{-}\!2$};
\node[elbl] at (-0.314,-0.241) [yshift=-4.8pt] {$1\!{-}\!5$};
\node[elbl] at (-0.286,-0.310) [yshift=-4.8pt] {$1\!{-}\!6$};
\node[elbl] at (-0.240,-0.368) [yshift=-4.8pt] {$1\!{-}\!7$};
\node[elbl] at (0.292,-0.264) [yshift=-4.8pt] {$2\!{-}\!3$};
\node[elbl] at (0.260,-0.368) [yshift=-4.8pt] {$2\!{-}\!7$};
\node[elbl] at (0.319,-0.339) [yshift=-4.8pt] {$2\!{-}\!8$};
\node[elbl] at (0.033,-0.035) [yshift=-4.8pt] {$3\!{-}\!4$};
\node[elbl] at (0.111,-0.171) [yshift=-4.8pt] {$3\!{-}\!8$};
\node[elbl] at (0.072,-0.136) [yshift=-4.8pt] {$3\!{-}\!9$};
\node[elbl] at (-0.073,-0.011) [yshift=-4.8pt] {$4\!{-}\!5$};
\node[elbl] at (-0.100,-0.117) [yshift=-4.8pt] {$5\!{-}\!6$};
\node[elbl] at (-0.027,-0.245) [yshift=-4.8pt] {$6\!{-}\!7$};
\node[elbl] at (0.079,-0.274) [yshift=-4.8pt] {$7\!{-}\!8$};
\node[elbl] at (0.099,-0.211) [yshift=-4.8pt] {$8\!{-}\!9$};
\node[elbl] at (0.059,-0.122) [yshift=-4.8pt] {$10\!{-}\!3$};
\node[elbl] at (0.047,-0.162) [yshift=-4.8pt] {$10\!{-}\!9$};
\node[elbl] at (0.029,-0.139) [yshift=-4.8pt] {$10\!{-}\!11$};
\node[elbl] at (0.016,-0.152) [yshift=-4.8pt] {$10\!{-}\!17$};
\node[elbl] at (0.022,-0.162) [yshift=-4.8pt] {$10\!{-}\!18$};
\node[elbl] at (0.054,-0.113) [yshift=-4.8pt] {$11\!{-}\!3$};
\node[elbl] at (0.019,-0.122) [yshift=-4.8pt] {$11\!{-}\!12$};
\node[elbl] at (0.006,-0.136) [yshift=-4.8pt] {$11\!{-}\!16$};
\node[elbl] at (0.010,-0.144) [yshift=-4.8pt] {$11\!{-}\!17$};
\node[elbl] at (0.049,-0.104) [yshift=-4.8pt] {$12\!{-}\!3$};
\node[elbl] at (0.008,-0.095) [yshift=-4.8pt] {$12\!{-}\!13$};
\node[elbl] at (-0.004,-0.116) [yshift=-4.8pt] {$12\!{-}\!15$};
\node[elbl] at (0.001,-0.127) [yshift=-4.8pt] {$12\!{-}\!16$};
\node[elbl] at (0.043,-0.086) [yshift=-4.8pt] {$13\!{-}\!3$};
\node[elbl] at (-0.008,-0.025) [yshift=-4.8pt] {$13\!{-}\!4$};
\node[elbl] at (-0.023,-0.079) [yshift=-4.8pt] {$13\!{-}\!14$};
\node[elbl] at (-0.011,-0.098) [yshift=-4.8pt] {$13\!{-}\!15$};
\node[elbl] at (-0.033,-0.027) [yshift=-4.8pt] {$14\!{-}\!4$};
\node[elbl] at (-0.088,-0.064) [yshift=-4.8pt] {$14\!{-}\!5$};
\node[elbl] at (-0.060,-0.134) [yshift=-4.8pt] {$14\!{-}\!6$};
\node[elbl] at (-0.035,-0.100) [yshift=-4.8pt] {$14\!{-}\!15$};
\node[elbl] at (-0.048,-0.153) [yshift=-4.8pt] {$15\!{-}\!6$};
\node[elbl] at (-0.018,-0.130) [yshift=-4.8pt] {$15\!{-}\!16$};
\node[elbl] at (-0.042,-0.164) [yshift=-4.8pt] {$16\!{-}\!6$};
\node[elbl] at (-0.008,-0.149) [yshift=-4.8pt] {$16\!{-}\!17$};
\node[elbl] at (-0.038,-0.171) [yshift=-4.8pt] {$17\!{-}\!6$};
\node[elbl] at (0.003,-0.167) [yshift=-4.8pt] {$17\!{-}\!18$};
\node[elbl] at (-0.031,-0.182) [yshift=-4.8pt] {$18\!{-}\!6$};
\node[elbl] at (0.035,-0.177) [yshift=-4.8pt] {$18\!{-}\!9$};
\node[elbl] at (0.021,-0.197) [yshift=-4.8pt] {$18\!{-}\!19$};
\node[elbl] at (-0.021,-0.202) [yshift=-4.8pt] {$19\!{-}\!6$};
\node[elbl] at (0.025,-0.260) [yshift=-4.8pt] {$19\!{-}\!7$};
\node[elbl] at (0.085,-0.231) [yshift=-4.8pt] {$19\!{-}\!8$};
\node[elbl] at (0.045,-0.197) [yshift=-4.8pt] {$19\!{-}\!9$};
\node[dlbl] at (-0.250,0.000) [yshift=5.0pt] {4};
\node[dlbl] at (-0.009,0.230) [yshift=5.0pt] {2};
\node[dlbl] at (-0.286,-0.310) [yshift=5.0pt] {6};
\node[dlbl] at (0.292,-0.264) [yshift=5.0pt] {3,18};
\node[dlbl] at (0.260,-0.368) [yshift=5.0pt] {5,17};
\node[dlbl] at (0.099,-0.211) [yshift=5.0pt] {13,14};
\node[dlbl] at (0.047,-0.162) [yshift=5.0pt] {11,12};
\node[dlbl] at (0.029,-0.139) [yshift=5.0pt] {7,8};
\node[dlbl] at (0.019,-0.122) [yshift=5.0pt] {5,6};
\node[dlbl] at (0.043,-0.086) [yshift=5.0pt] {1,2};
\node[dlbl] at (-0.011,-0.098) [yshift=5.0pt] {3,13};
\node[dlbl] at (-0.033,-0.027) [yshift=5.0pt] {0};
\node[dlbl] at (-0.060,-0.134) [yshift=5.0pt] {12};
\node[dlbl] at (-0.018,-0.130) [yshift=5.0pt] {4,11};
\node[dlbl] at (-0.008,-0.149) [yshift=5.0pt] {9,10};
\node[dlbl] at (0.003,-0.167) [yshift=5.0pt] {9,10};
\node[dlbl] at (0.021,-0.197) [yshift=5.0pt] {8,15};
\node[dlbl] at (0.025,-0.260) [yshift=5.0pt] {7,16};
\draw[cut] (-0.075,-0.032)--(-0.101,-0.097);
\node[cutlbl] at (-0.120,-0.142) {cut 1};
\draw[cut] (-0.026,-0.044)--(-0.020,-0.114);
\node[cutlbl] at (-0.016,-0.162) {cut 2};
\draw[cut] (0.058,-0.138)--(0.041,-0.070);
\node[cutlbl] at (0.030,-0.023) {cut 3};
\draw[cut] (-0.004,-0.102)--(0.016,-0.169);
\node[cutlbl] at (0.029,-0.217) {cut 4};
\draw[cut] (0.085,-0.146)--(0.034,-0.097);
\node[cutlbl] at (-0.001,-0.063) {cut 5};
\draw[cut] (0.035,-0.212)--(0.035,-0.142);
\node[cutlbl] at (0.034,-0.093) {cut 6};
\draw[cut] (0.079,-0.183)--(0.144,-0.158);
\node[cutlbl] at (0.190,-0.142) {cut 7};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize medial graph $M(G)$ at edge midpoints}
\end{tabular}
@@ -0,0 +1,382 @@
% whole medial graph: n=20 seed=72 source=9 recognised treads=[2] |M(G)|=54
\begin{tabular}{c}
\begin{tikzpicture}[scale=4.06,
sedge/.style={black!50, line width=0.35pt},
sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},
srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]
\draw[sedge] (0.000,0.433)--(-0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.027,-0.257);
\draw[sedge] (0.000,0.433)--(0.199,-0.203);
\draw[sedge] (0.000,0.433)--(-0.158,-0.086);
\draw[sedge] (-0.500,-0.433)--(0.500,-0.433);
\draw[sedge] (-0.500,-0.433)--(0.027,-0.257);
\draw[sedge] (-0.500,-0.433)--(0.012,-0.355);
\draw[sedge] (-0.500,-0.433)--(-0.170,-0.348);
\draw[sedge] (-0.500,-0.433)--(-0.218,-0.346);
\draw[sedge] (-0.500,-0.433)--(-0.158,-0.086);
\draw[sedge] (-0.500,-0.433)--(-0.230,-0.345);
\draw[sedge] (0.500,-0.433)--(0.027,-0.257);
\draw[sedge] (0.500,-0.433)--(0.199,-0.203);
\draw[sedge] (0.500,-0.433)--(0.012,-0.355);
\draw[sedge] (0.500,-0.433)--(0.234,-0.280);
\draw[sedge] (0.500,-0.433)--(0.292,-0.291);
\draw[sedge] (0.500,-0.433)--(0.151,-0.341);
\draw[sedge] (0.500,-0.433)--(0.254,-0.323);
\draw[sedge] (0.027,-0.257)--(0.199,-0.203);
\draw[sedge] (0.027,-0.257)--(0.012,-0.355);
\draw[sedge] (0.027,-0.257)--(0.234,-0.280);
\draw[sedge] (0.027,-0.257)--(-0.170,-0.348);
\draw[sedge] (0.027,-0.257)--(0.151,-0.341);
\draw[sedge] (0.027,-0.257)--(0.254,-0.323);
\draw[sedge] (0.027,-0.257)--(-0.218,-0.346);
\draw[sedge] (0.027,-0.257)--(0.063,-0.317);
\draw[sedge] (0.027,-0.257)--(0.146,-0.243);
\draw[sedge] (0.027,-0.257)--(-0.158,-0.086);
\draw[sedge] (0.027,-0.257)--(0.124,-0.234);
\draw[sedge] (0.027,-0.257)--(-0.230,-0.345);
\draw[sedge] (0.199,-0.203)--(0.234,-0.280);
\draw[sedge] (0.199,-0.203)--(0.292,-0.291);
\draw[sedge] (0.199,-0.203)--(0.233,-0.249);
\draw[sedge] (0.199,-0.203)--(0.221,-0.241);
\draw[sedge] (0.199,-0.203)--(0.146,-0.243);
\draw[sedge] (0.199,-0.203)--(0.218,-0.231);
\draw[sedge] (0.199,-0.203)--(0.124,-0.234);
\draw[sedge] (0.012,-0.355)--(-0.170,-0.348);
\draw[sedge] (0.012,-0.355)--(0.151,-0.341);
\draw[sedge] (0.012,-0.355)--(0.063,-0.317);
\draw[sedge] (0.234,-0.280)--(0.292,-0.291);
\draw[sedge] (0.234,-0.280)--(0.233,-0.249);
\draw[sedge] (0.234,-0.280)--(0.254,-0.323);
\draw[sedge] (0.234,-0.280)--(0.221,-0.241);
\draw[sedge] (0.234,-0.280)--(0.146,-0.243);
\draw[sedge] (0.292,-0.291)--(0.233,-0.249);
\draw[sedge] (0.233,-0.249)--(0.221,-0.241);
\draw[sedge] (0.233,-0.249)--(0.218,-0.231);
\draw[sedge] (-0.170,-0.348)--(-0.218,-0.346);
\draw[sedge] (0.151,-0.341)--(0.063,-0.317);
\draw[sedge] (-0.218,-0.346)--(-0.230,-0.345);
\draw[sedge] (0.221,-0.241)--(0.218,-0.231);
\draw[sedge] (0.146,-0.243)--(0.124,-0.234);
\node[sv] at (0.000,0.433) {};
\node[sv] at (-0.500,-0.433) {};
\node[sv] at (0.500,-0.433) {};
\node[sv] at (0.027,-0.257) {};
\node[sv] at (0.199,-0.203) {};
\node[sv] at (0.012,-0.355) {};
\node[sv] at (0.234,-0.280) {};
\node[sv] at (0.292,-0.291) {};
\node[sv] at (0.233,-0.249) {};
\node[srcv] at (-0.170,-0.348) {};
\node[sv] at (0.151,-0.341) {};
\node[sv] at (0.254,-0.323) {};
\node[sv] at (-0.218,-0.346) {};
\node[sv] at (0.221,-0.241) {};
\node[sv] at (0.063,-0.317) {};
\node[sv] at (0.146,-0.243) {};
\node[sv] at (0.218,-0.231) {};
\node[sv] at (-0.158,-0.086) {};
\node[sv] at (0.124,-0.234) {};
\node[sv] at (-0.230,-0.345) {};
\node[font=\scriptsize, text=blue!70!black] at (-0.170,-0.433) {source 9};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize source graph $G$}
\\[1.0ex]
\begin{tikzpicture}[scale=7.0,
base/.style={black!12, line width=0.25pt},
med/.style={black!38, line width=0.32pt},
annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},
levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},
levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},
levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},
knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},
elbl/.style={font=\tiny, text=black!70, inner sep=0.2pt},
dlbl/.style={font=\tiny\bfseries, text=black, inner sep=0.5pt},
cut/.style={red!80!black, line width=1.0pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[base] (0.000,0.433)--(-0.500,-0.433);
\draw[base] (0.000,0.433)--(0.500,-0.433);
\draw[base] (0.000,0.433)--(0.027,-0.257);
\draw[base] (0.000,0.433)--(0.199,-0.203);
\draw[base] (0.000,0.433)--(-0.158,-0.086);
\draw[base] (-0.500,-0.433)--(0.500,-0.433);
\draw[base] (-0.500,-0.433)--(0.027,-0.257);
\draw[base] (-0.500,-0.433)--(0.012,-0.355);
\draw[base] (-0.500,-0.433)--(-0.170,-0.348);
\draw[base] (-0.500,-0.433)--(-0.218,-0.346);
\draw[base] (-0.500,-0.433)--(-0.158,-0.086);
\draw[base] (-0.500,-0.433)--(-0.230,-0.345);
\draw[base] (0.500,-0.433)--(0.027,-0.257);
\draw[base] (0.500,-0.433)--(0.199,-0.203);
\draw[base] (0.500,-0.433)--(0.012,-0.355);
\draw[base] (0.500,-0.433)--(0.234,-0.280);
\draw[base] (0.500,-0.433)--(0.292,-0.291);
\draw[base] (0.500,-0.433)--(0.151,-0.341);
\draw[base] (0.500,-0.433)--(0.254,-0.323);
\draw[base] (0.027,-0.257)--(0.199,-0.203);
\draw[base] (0.027,-0.257)--(0.012,-0.355);
\draw[base] (0.027,-0.257)--(0.234,-0.280);
\draw[base] (0.027,-0.257)--(-0.170,-0.348);
\draw[base] (0.027,-0.257)--(0.151,-0.341);
\draw[base] (0.027,-0.257)--(0.254,-0.323);
\draw[base] (0.027,-0.257)--(-0.218,-0.346);
\draw[base] (0.027,-0.257)--(0.063,-0.317);
\draw[base] (0.027,-0.257)--(0.146,-0.243);
\draw[base] (0.027,-0.257)--(-0.158,-0.086);
\draw[base] (0.027,-0.257)--(0.124,-0.234);
\draw[base] (0.027,-0.257)--(-0.230,-0.345);
\draw[base] (0.199,-0.203)--(0.234,-0.280);
\draw[base] (0.199,-0.203)--(0.292,-0.291);
\draw[base] (0.199,-0.203)--(0.233,-0.249);
\draw[base] (0.199,-0.203)--(0.221,-0.241);
\draw[base] (0.199,-0.203)--(0.146,-0.243);
\draw[base] (0.199,-0.203)--(0.218,-0.231);
\draw[base] (0.199,-0.203)--(0.124,-0.234);
\draw[base] (0.012,-0.355)--(-0.170,-0.348);
\draw[base] (0.012,-0.355)--(0.151,-0.341);
\draw[base] (0.012,-0.355)--(0.063,-0.317);
\draw[base] (0.234,-0.280)--(0.292,-0.291);
\draw[base] (0.234,-0.280)--(0.233,-0.249);
\draw[base] (0.234,-0.280)--(0.254,-0.323);
\draw[base] (0.234,-0.280)--(0.221,-0.241);
\draw[base] (0.234,-0.280)--(0.146,-0.243);
\draw[base] (0.292,-0.291)--(0.233,-0.249);
\draw[base] (0.233,-0.249)--(0.221,-0.241);
\draw[base] (0.233,-0.249)--(0.218,-0.231);
\draw[base] (-0.170,-0.348)--(-0.218,-0.346);
\draw[base] (0.151,-0.341)--(0.063,-0.317);
\draw[base] (-0.218,-0.346)--(-0.230,-0.345);
\draw[base] (0.221,-0.241)--(0.218,-0.231);
\draw[base] (0.146,-0.243)--(0.124,-0.234);
\draw[med] (-0.250,0.000)--(-0.079,0.174);
\draw[med] (-0.250,0.000)--(0.250,0.000);
\draw[med] (-0.250,0.000)--(0.000,-0.433);
\draw[med] (-0.250,0.000)--(-0.329,-0.259);
\draw[med] (0.250,0.000)--(0.100,0.115);
\draw[med] (0.250,0.000)--(0.000,-0.433);
\draw[med] (0.250,0.000)--(0.350,-0.318);
\draw[med] (0.014,0.088)--(-0.079,0.174);
\draw[med] (0.014,0.088)--(0.100,0.115);
\draw[med] (0.014,0.088)--(0.113,-0.230);
\draw[med] (0.014,0.088)--(-0.065,-0.171);
\draw[med] (0.100,0.115)--(0.350,-0.318);
\draw[med] (0.100,0.115)--(0.113,-0.230);
\draw[med] (-0.079,0.174)--(-0.065,-0.171);
\draw[med] (-0.079,0.174)--(-0.329,-0.259);
\draw[med] (0.000,-0.433)--(-0.244,-0.394);
\draw[med] (0.000,-0.433)--(0.256,-0.394);
\draw[med] (-0.236,-0.345)--(-0.365,-0.389);
\draw[med] (-0.236,-0.345)--(-0.329,-0.259);
\draw[med] (-0.236,-0.345)--(-0.065,-0.171);
\draw[med] (-0.236,-0.345)--(-0.102,-0.301);
\draw[med] (-0.244,-0.394)--(-0.335,-0.390);
\draw[med] (-0.244,-0.394)--(-0.079,-0.351);
\draw[med] (-0.244,-0.394)--(0.256,-0.394);
\draw[med] (-0.335,-0.390)--(-0.359,-0.389);
\draw[med] (-0.335,-0.390)--(-0.194,-0.347);
\draw[med] (-0.335,-0.390)--(-0.079,-0.351);
\draw[med] (-0.359,-0.389)--(-0.365,-0.389);
\draw[med] (-0.359,-0.389)--(-0.224,-0.345);
\draw[med] (-0.359,-0.389)--(-0.194,-0.347);
\draw[med] (-0.329,-0.259)--(-0.065,-0.171);
\draw[med] (-0.365,-0.389)--(-0.102,-0.301);
\draw[med] (-0.365,-0.389)--(-0.224,-0.345);
\draw[med] (0.264,-0.345)--(0.377,-0.378);
\draw[med] (0.264,-0.345)--(0.325,-0.387);
\draw[med] (0.264,-0.345)--(0.140,-0.290);
\draw[med] (0.264,-0.345)--(0.089,-0.299);
\draw[med] (0.350,-0.318)--(0.396,-0.362);
\draw[med] (0.350,-0.318)--(0.245,-0.247);
\draw[med] (0.256,-0.394)--(0.325,-0.387);
\draw[med] (0.256,-0.394)--(0.081,-0.348);
\draw[med] (0.367,-0.357)--(0.396,-0.362);
\draw[med] (0.367,-0.357)--(0.377,-0.378);
\draw[med] (0.367,-0.357)--(0.244,-0.302);
\draw[med] (0.367,-0.357)--(0.263,-0.286);
\draw[med] (0.396,-0.362)--(0.263,-0.286);
\draw[med] (0.396,-0.362)--(0.245,-0.247);
\draw[med] (0.325,-0.387)--(0.081,-0.348);
\draw[med] (0.325,-0.387)--(0.089,-0.299);
\draw[med] (0.377,-0.378)--(0.140,-0.290);
\draw[med] (0.377,-0.378)--(0.244,-0.302);
\draw[med] (0.113,-0.230)--(0.076,-0.246);
\draw[med] (0.113,-0.230)--(0.162,-0.218);
\draw[med] (0.019,-0.306)--(-0.071,-0.302);
\draw[med] (0.019,-0.306)--(0.045,-0.287);
\draw[med] (0.019,-0.306)--(-0.079,-0.351);
\draw[med] (0.019,-0.306)--(0.038,-0.336);
\draw[med] (0.131,-0.268)--(0.140,-0.290);
\draw[med] (0.131,-0.268)--(0.087,-0.250);
\draw[med] (0.131,-0.268)--(0.190,-0.262);
\draw[med] (0.131,-0.268)--(0.244,-0.302);
\draw[med] (-0.071,-0.302)--(-0.096,-0.301);
\draw[med] (-0.071,-0.302)--(-0.079,-0.351);
\draw[med] (-0.071,-0.302)--(-0.194,-0.347);
\draw[med] (0.089,-0.299)--(0.045,-0.287);
\draw[med] (0.089,-0.299)--(0.107,-0.329);
\draw[med] (0.140,-0.290)--(0.244,-0.302);
\draw[med] (-0.096,-0.301)--(-0.102,-0.301);
\draw[med] (-0.096,-0.301)--(-0.194,-0.347);
\draw[med] (-0.096,-0.301)--(-0.224,-0.345);
\draw[med] (0.045,-0.287)--(0.107,-0.329);
\draw[med] (0.045,-0.287)--(0.038,-0.336);
\draw[med] (0.087,-0.250)--(0.076,-0.246);
\draw[med] (0.087,-0.250)--(0.135,-0.239);
\draw[med] (0.087,-0.250)--(0.190,-0.262);
\draw[med] (0.076,-0.246)--(0.162,-0.218);
\draw[med] (0.076,-0.246)--(0.135,-0.239);
\draw[med] (-0.102,-0.301)--(-0.224,-0.345);
\draw[med] (0.217,-0.241)--(0.173,-0.223);
\draw[med] (0.217,-0.241)--(0.210,-0.222);
\draw[med] (0.217,-0.241)--(0.190,-0.262);
\draw[med] (0.217,-0.241)--(0.227,-0.260);
\draw[med] (0.245,-0.247)--(0.216,-0.226);
\draw[med] (0.245,-0.247)--(0.262,-0.270);
\draw[med] (0.216,-0.226)--(0.209,-0.217);
\draw[med] (0.216,-0.226)--(0.262,-0.270);
\draw[med] (0.216,-0.226)--(0.225,-0.240);
\draw[med] (0.210,-0.222)--(0.209,-0.217);
\draw[med] (0.210,-0.222)--(0.219,-0.236);
\draw[med] (0.210,-0.222)--(0.227,-0.260);
\draw[med] (0.173,-0.223)--(0.162,-0.218);
\draw[med] (0.173,-0.223)--(0.190,-0.262);
\draw[med] (0.173,-0.223)--(0.135,-0.239);
\draw[med] (0.209,-0.217)--(0.225,-0.240);
\draw[med] (0.209,-0.217)--(0.219,-0.236);
\draw[med] (0.162,-0.218)--(0.135,-0.239);
\draw[med] (0.081,-0.348)--(0.038,-0.336);
\draw[med] (0.081,-0.348)--(0.107,-0.329);
\draw[med] (0.038,-0.336)--(0.107,-0.329);
\draw[med] (0.263,-0.286)--(0.233,-0.265);
\draw[med] (0.263,-0.286)--(0.262,-0.270);
\draw[med] (0.233,-0.265)--(0.227,-0.260);
\draw[med] (0.233,-0.265)--(0.227,-0.245);
\draw[med] (0.233,-0.265)--(0.262,-0.270);
\draw[med] (0.227,-0.260)--(0.227,-0.245);
\draw[med] (0.227,-0.245)--(0.225,-0.240);
\draw[med] (0.227,-0.245)--(0.219,-0.236);
\draw[med] (0.225,-0.240)--(0.219,-0.236);
\node[annv] at (-0.250,0.000) {};
\node[levtwo] at (0.250,0.000) {};
\node[annv] at (0.014,0.088) {};
\node[levtwo] at (0.100,0.115) {};
\node[levtwo] at (-0.079,0.174) {};
\node[annv] at (0.000,-0.433) {};
\node[levone] at (-0.236,-0.345) {};
\node[levone] at (-0.244,-0.394) {};
\node[annv] at (-0.335,-0.390) {};
\node[levone] at (-0.359,-0.389) {};
\node[annv] at (-0.329,-0.259) {};
\node[annv] at (-0.365,-0.389) {};
\node[annv] at (0.264,-0.345) {};
\node[knownv] at (0.350,-0.318) {};
\node[annv] at (0.256,-0.394) {};
\node[knownv] at (0.367,-0.357) {};
\node[annv] at (0.396,-0.362) {};
\node[annv] at (0.113,-0.230) {};
\node[levone] at (0.019,-0.306) {};
\node[annv] at (0.131,-0.268) {};
\node[annv] at (-0.071,-0.302) {};
\node[knownv] at (0.217,-0.241) {};
\node[annv] at (0.245,-0.247) {};
\node[annv] at (0.216,-0.226) {};
\node[annv] at (-0.079,-0.351) {};
\node[annv] at (0.263,-0.286) {};
\node[annv] at (0.233,-0.265) {};
\node[knownv] at (0.262,-0.270) {};
\node[levtwo] at (0.325,-0.387) {};
\node[annv] at (0.089,-0.299) {};
\node[annv] at (0.081,-0.348) {};
\node[levtwo] at (0.107,-0.329) {};
\node[levtwo] at (0.377,-0.378) {};
\node[annv] at (0.140,-0.290) {};
\node[levtwo] at (0.244,-0.302) {};
\node[levone] at (-0.096,-0.301) {};
\node[annv] at (-0.194,-0.347) {};
\node[annv] at (-0.224,-0.345) {};
\node[annv] at (0.210,-0.222) {};
\node[annv] at (0.227,-0.260) {};
\node[knownv] at (0.227,-0.245) {};
\node[knownv] at (0.219,-0.236) {};
\node[annv] at (0.045,-0.287) {};
\node[annv] at (0.038,-0.336) {};
\node[annv] at (0.087,-0.250) {};
\node[levtwo] at (0.173,-0.223) {};
\node[levtwo] at (0.190,-0.262) {};
\node[levtwo] at (0.135,-0.239) {};
\node[annv] at (0.209,-0.217) {};
\node[knownv] at (0.225,-0.240) {};
\node[annv] at (-0.065,-0.171) {};
\node[annv] at (0.076,-0.246) {};
\node[levtwo] at (0.162,-0.218) {};
\node[annv] at (-0.102,-0.301) {};
\node[elbl] at (-0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!1$};
\node[elbl] at (0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!2$};
\node[elbl] at (0.014,0.088) [yshift=-4.8pt] {$0\!{-}\!3$};
\node[elbl] at (0.100,0.115) [yshift=-4.8pt] {$0\!{-}\!4$};
\node[elbl] at (-0.079,0.174) [yshift=-4.8pt] {$0\!{-}\!17$};
\node[elbl] at (0.000,-0.433) [yshift=-4.8pt] {$1\!{-}\!2$};
\node[elbl] at (-0.236,-0.345) [yshift=-4.8pt] {$1\!{-}\!3$};
\node[elbl] at (-0.244,-0.394) [yshift=-4.8pt] {$1\!{-}\!5$};
\node[elbl] at (-0.335,-0.390) [yshift=-4.8pt] {$1\!{-}\!9$};
\node[elbl] at (-0.359,-0.389) [yshift=-4.8pt] {$1\!{-}\!12$};
\node[elbl] at (-0.329,-0.259) [yshift=-4.8pt] {$1\!{-}\!17$};
\node[elbl] at (-0.365,-0.389) [yshift=-4.8pt] {$1\!{-}\!19$};
\node[elbl] at (0.264,-0.345) [yshift=-4.8pt] {$2\!{-}\!3$};
\node[elbl] at (0.350,-0.318) [yshift=-4.8pt] {$2\!{-}\!4$};
\node[elbl] at (0.256,-0.394) [yshift=-4.8pt] {$2\!{-}\!5$};
\node[elbl] at (0.367,-0.357) [yshift=-4.8pt] {$2\!{-}\!6$};
\node[elbl] at (0.396,-0.362) [yshift=-4.8pt] {$2\!{-}\!7$};
\node[elbl] at (0.113,-0.230) [yshift=-4.8pt] {$3\!{-}\!4$};
\node[elbl] at (0.019,-0.306) [yshift=-4.8pt] {$3\!{-}\!5$};
\node[elbl] at (0.131,-0.268) [yshift=-4.8pt] {$3\!{-}\!6$};
\node[elbl] at (-0.071,-0.302) [yshift=-4.8pt] {$3\!{-}\!9$};
\node[elbl] at (0.217,-0.241) [yshift=-4.8pt] {$4\!{-}\!6$};
\node[elbl] at (0.245,-0.247) [yshift=-4.8pt] {$4\!{-}\!7$};
\node[elbl] at (0.216,-0.226) [yshift=-4.8pt] {$4\!{-}\!8$};
\node[elbl] at (-0.079,-0.351) [yshift=-4.8pt] {$5\!{-}\!9$};
\node[elbl] at (0.263,-0.286) [yshift=-4.8pt] {$6\!{-}\!7$};
\node[elbl] at (0.233,-0.265) [yshift=-4.8pt] {$6\!{-}\!8$};
\node[elbl] at (0.262,-0.270) [yshift=-4.8pt] {$7\!{-}\!8$};
\node[elbl] at (0.325,-0.387) [yshift=-4.8pt] {$10\!{-}\!2$};
\node[elbl] at (0.089,-0.299) [yshift=-4.8pt] {$10\!{-}\!3$};
\node[elbl] at (0.081,-0.348) [yshift=-4.8pt] {$10\!{-}\!5$};
\node[elbl] at (0.107,-0.329) [yshift=-4.8pt] {$10\!{-}\!14$};
\node[elbl] at (0.377,-0.378) [yshift=-4.8pt] {$11\!{-}\!2$};
\node[elbl] at (0.140,-0.290) [yshift=-4.8pt] {$11\!{-}\!3$};
\node[elbl] at (0.244,-0.302) [yshift=-4.8pt] {$11\!{-}\!6$};
\node[elbl] at (-0.096,-0.301) [yshift=-4.8pt] {$12\!{-}\!3$};
\node[elbl] at (-0.194,-0.347) [yshift=-4.8pt] {$12\!{-}\!9$};
\node[elbl] at (-0.224,-0.345) [yshift=-4.8pt] {$12\!{-}\!19$};
\node[elbl] at (0.210,-0.222) [yshift=-4.8pt] {$13\!{-}\!4$};
\node[elbl] at (0.227,-0.260) [yshift=-4.8pt] {$13\!{-}\!6$};
\node[elbl] at (0.227,-0.245) [yshift=-4.8pt] {$13\!{-}\!8$};
\node[elbl] at (0.219,-0.236) [yshift=-4.8pt] {$13\!{-}\!16$};
\node[elbl] at (0.045,-0.287) [yshift=-4.8pt] {$14\!{-}\!3$};
\node[elbl] at (0.038,-0.336) [yshift=-4.8pt] {$14\!{-}\!5$};
\node[elbl] at (0.087,-0.250) [yshift=-4.8pt] {$15\!{-}\!3$};
\node[elbl] at (0.173,-0.223) [yshift=-4.8pt] {$15\!{-}\!4$};
\node[elbl] at (0.190,-0.262) [yshift=-4.8pt] {$15\!{-}\!6$};
\node[elbl] at (0.135,-0.239) [yshift=-4.8pt] {$15\!{-}\!18$};
\node[elbl] at (0.209,-0.217) [yshift=-4.8pt] {$16\!{-}\!4$};
\node[elbl] at (0.225,-0.240) [yshift=-4.8pt] {$16\!{-}\!8$};
\node[elbl] at (-0.065,-0.171) [yshift=-4.8pt] {$17\!{-}\!3$};
\node[elbl] at (0.076,-0.246) [yshift=-4.8pt] {$18\!{-}\!3$};
\node[elbl] at (0.162,-0.218) [yshift=-4.8pt] {$18\!{-}\!4$};
\node[elbl] at (-0.102,-0.301) [yshift=-4.8pt] {$19\!{-}\!3$};
\node[dlbl] at (0.350,-0.318) [yshift=5.0pt] {6};
\node[dlbl] at (0.367,-0.357) [yshift=5.0pt] {7};
\node[dlbl] at (0.217,-0.241) [yshift=5.0pt] {0};
\node[dlbl] at (0.262,-0.270) [yshift=5.0pt] {2,3};
\node[dlbl] at (0.227,-0.245) [yshift=5.0pt] {1};
\node[dlbl] at (0.219,-0.236) [yshift=5.0pt] {5};
\node[dlbl] at (0.225,-0.240) [yshift=5.0pt] {4};
\draw[cut] (0.241,-0.204)--(0.180,-0.239);
\node[cutlbl] at (0.137,-0.263) {cut 1};
\draw[cut] (0.256,-0.320)--(0.269,-0.251);
\node[cutlbl] at (0.279,-0.203) {cut 2};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize medial graph $M(G)$ at edge midpoints}
\end{tabular}
@@ -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 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

@@ -0,0 +1,283 @@
# Chained seams: the medial pigeonhole, located
Companion to the interface-alphabet results (`kempe_interface_admissibility_probe.py`,
`kempe_tile_overlap_probe.py`). This note pursues the paper's §6 *medial pigeonhole
programme* — the restriction relation `R_T` between a tire's outer and inner boundary
states, and the open *chain-pigeonhole conjecture* — at the data level.
Scripts: `kempe_transfer_relation_probe.py`, `kempe_uniform_family_probe.py`.
## Background
A nested chain `T_0 ⊃ T_1 ⊃ …` glues into a proper 3-colouring of `M(G)` iff
consecutive boundary states match: `inner-state(T_i) = outer-state(T_{i+1})`
(compatible family, paper Prop "gluing criterion"). Boundary states are necklaces
(colours permuted, boundary walk rotated/reflected). The restriction relation
```
R_T ⊆ outer-states × inner-states
```
records which (outer, inner) boundary-state pairs one Kempe-balanced colouring of `T`
realises jointly. The chain-pigeonhole conjecture asks whether nested `R_T`'s can ever
compose to empty.
Established earlier: the realised boundary alphabet (each side) is exactly the full
parity-admissible set, identical inner vs outer, `n`-independent (n=9, n=12, m=3..8);
and every *pair* of tiles overlaps, so single seams never dead-end.
## Finding 1 — `R_T` is genuinely coupled
`R_T` is **not** a product of its projections: a tile's inner boundary necklace
constrains its outer one. Seen at n=9 in the `(p,q) = (4,5)` and `(5,4)` size classes
(`product? = False`), and broadly at n=12. So "every seam individually realisable"
does **not** trivially pass through a tile.
## Finding 2 — the uniform shortcut works at n=9, breaks at n=12
Question: is there one boundary state `σ_m` per level-cycle *size* that threads every
tile simultaneously (outer face and every inner face)? If so, painting every level
cycle `σ_m` glues any tree with no pigeonhole.
- **n=9: FEASIBLE.** Unique universal per size (`|D[m]| = 1`), and notably *not*
monochromatic — the balanced-block necklaces
```
σ3 = 012 σ4 = 0011 σ5 = 00012 σ6 = 000011
```
threads all 66 tiles. (Caveat: n=9 has **no** branching tiles.)
- **n=12: INFEASIBLE.** 1237 tiles (1029 bite, 175 genuinely branching). The CSP fails
for one sharp reason: **size-7 seams admit no universal state** (`|D[7]| = 0`). The
211 size-7 boundaries realise all 10 admissible necklaces between them, but their
intersection is empty — the near-universal `0001112` lands on **210 of 211**,
blocked by a single tile.
Per-size universal domain sizes at n=12: `3:1 4:1 5:1 6:2 7:0 8:4 9:1`.
## Finding 3 — the failure is SPORADIC, not a monotone trend
`kempe_universal_trend_probe.py` tracks `|D[m]|` (universal count) per size across
`n = 6..13`. Legend `|D[m]| (best_coverage / num_boundaries)`; `*` = odd size:
```
n= 6: m3*=1(6/6) m4=2(2/2) m6=4(1/1)
n= 7: m3*=1(10/10) m4=1(8/8) m5*=1(2/2) m7*=3(1/1)
n= 8: m3*=1(28/28) m4=2(21/21) m5*=1(10/10) m6=2(3/3) m8=8(1/1)
n= 9: m3*=1(49/49) m4=1(52/52) m5*=1(28/28) m6=1(14/14) m7*=2(3/3) m9*=8(1/1)
n=10: m3*=1(118) m4=2(106) m5*=1(86) m6=2(46) m7*=1(16/16) m8=4(4/4) m10=18(1/1)
n=11: m3*=1(310) m4=1(263) m5*=1(188) m6=1(139) m7*=2(60/60) m8=2(20/20) m9*=3(4/4) m11*=21(1/1)
n=12: m3*=1(849) m4=1(691) m5*=1(534) m6=2(353) m7*=0(210/211) m8=3(88) m9*=1(24/24) m10=6(5/5) m12=48(1/1)
n=13: m3*=1(2457) m4=1(1805) m5*=1(1386) m6=1(1070) m7*=2(579/579) m8=1(315) m9*=2(110) m10=1(28) m11*=7(5) m13*=63(1/1)
```
The earlier "universals vanish as the tile population grows" reading is **wrong**.
Across all `n = 6..13` and all sizes, the **only** empty universal anywhere is
`(n=12, m=7)`. Size 7 has *more* boundaries at n=13 (579) than n=12 (211), yet its
universal is back (`|D|=2`). So the failure is **sporadic**, not monotone — a
number-theoretic quirk of `n=12`, not a scaling law. Note also that best_coverage is
always near-total: the most-shared necklace reaches all-but-rarely-one boundary, so
the universal is "almost there" even when it fails.
### The single breaker (n=12, m=7)
The lone size-7 boundary that kills the universal is the **outer rim** of
```
word = UUUDUDUDUDUD bite = (3,11) (7 up teeth)
```
— the most-alternating word with a near-full-span bite. Its outer rim realises 9 of
the 10 admissible size-7 necklaces, missing exactly `0001112` (the balanced two-block
`3+3+1` pattern); every other size-7 boundary realises `0001112`. One exceptional tile
breaks the shortcut.
## Finding 4 — interpretation
The uniform "paint every seam the same" shortcut **almost always works** (universals
non-empty with near-total coverage, even at thousands of boundaries), but is
**fragile**: a single exceptional tile can empty a per-size universal, as at
`(n=12, m=7)`. So the shortcut cannot be relied on in general — yet its failures are
rare, not systematic. The per-interface pigeonhole choice is needed precisely to
absorb these sporadic breakers. **This is not an obstruction to gluing** — pairwise
overlap always holds, so chains still glue by choosing states per interface. The
conjecture's difficulty is thus concentrated in rare exceptional tiles and the
branch-coupled selection around them, not in any single seam or in a scaling trend.
## Finding 5 — restricting to no separating triangles REMOVES the obstruction
The 4CT reduces to triangulations with no separating triangles (internally
4-connected). In the tread model, a separating (non-facial) triangle in `G` shows up
as a **length-3 boundary walk** of a tread: an outer rim of 3 up teeth, or an inner
face holding exactly 3 singleton down teeth (the `012` seam). Restricting to tiles
with **no length-3 boundary** (`kempe_universal_trend_probe.py --no-tri`,
`kempe_uniform_family_probe.py --no-tri`):
- The n=12 breaker `UUUDUDUDUDUD` bite=(3,11) has a size-3 inner face (the bite
encloses exactly `d5,d7,d9`), so it is **excluded** — along with its kin.
- The size-7 universal at n=12 is **restored**: `|D[7]|` goes `0 → 2`, on a reduced
population (211 → 76 size-7 boundaries). Across `n = 6..13` and all sizes, **every
`|D[m]| ≥ 1`** — no empty universal anywhere.
- The uniform-family CSP becomes **FEASIBLE at n=12** (was infeasible). The threading
family is now the simplest one — **monochromatic on even sizes, min-cut on odd**:
```
σ4 = 0000 σ5 = 00012 σ6 = 000000 σ7 = 0000012 σ8 = 00000000
```
(Without the restriction, `σ4` had to be `0011`, not monochromatic, because
separating-triangle tiles blocked the all-0 rim. Removing them lets monochromatic
even seams work.)
So the **only** universal failure we found (n=12, size 7) was an artifact of admitting
tiles that correspond to **non-4-connected** triangulations. On the 4CT-relevant class
(no separating triangles), the uniform seam family exists and gluing is constructively
trivial throughout the tested range — no pigeonhole needed.
Caveat: even at n=12 the restricted population has **0 branching tiles** (multi-inner
faces all require a size-3 face at this n), so the branching case stays untested under
the restriction; and this is `n ≤ 13` only.
## Finding 6 — smallest branching n (`kempe_branching_min_probe.py`)
Branching tile = `≥2` inner faces carrying singleton-down interfaces (a tree node with
`≥2` children).
```
n #tiles branching no-tri branching
9 81 0 0
10 203 0 0
11 503 30 0 <- unrestricted branching first appears
12 1344 175 0
13 3586 789 0
14 9929 3024 193 <- no-separating-triangle branching first appears
15 27481 10538 1022
```
- Unrestricted branching first appears at **n=11**.
- No-separating-triangle branching first appears at **n=14**. Smallest example:
```
word = UUUUDDDDDDDDDD bite = (8,13) p = 4
inner faces: root {4,5,6,7} (size 4) + bite(8,13) {9,10,11,12} (size 4)
```
4 up teeth, two size-4 inner faces — branching, no length-3 boundary. (Reason for the
gap: a branching node needs `≥2` inner faces each `≥4` singletons plus `≥4` up teeth
plus the bite pair = `4 + 4 + 4 + 2 = 14` edges.)
So **n=14** is the smallest place to test the uniform family / `R_T` composition on a
genuine *branching* no-separating-triangle tile — the conjecture's real case.
## Finding 7 — the branching case (n=14) is FEASIBLE with one regular family
Full uniform-family CSP at n=14 `--no-tri` (4403 tiles, 3766 bite, **193 branching**):
**FEASIBLE** — a single uniform family threads every tile, branching nodes included
(each branching tile shows the uniform state on its outer rim AND both inner faces at
once). Domains `|D[m]|`: `4:2 5:1 6:2 7:2 8:3 9:3 10:5`. The witness family is fully
regular:
```
σ_m = 0^m (monochromatic) if m even
σ_m = 0^(m-2) 1 2 (one block + 1 + 2) if m odd
4:0000 5:00012 6:000000 7:0000012 8:00000000 9:000000012 10:0000000000
```
A direct candidate test (just the 193 branching tiles) confirmed it independently:
the monochromatic-even / min-cut-odd family threads **193/193**.
So on the 4CT-relevant class (no separating triangles), the chained-seam pigeonhole is
**constructively resolved throughout the tested range** (n = 9, 12, 14, including the
first branching nodes) by one explicit regular seam family: paint every even level
cycle one colour, and every odd level cycle as a single monochromatic block plus the
two parity-forced off-colour vertices.
## Finding 8 — the regular family is REFUTED at n=15
`kempe_regular_family_test.py` (tests the fixed regular family with per-tile
early-exit). At n=12 it threads 614/614 (matches the CSP). **At n=15 it FAILS**, on two
distinct classes of no-separating-triangle tiles:
- **non-branching, large even outer + odd inner:** e.g. `UUUUUUDUDUDUDUD` (no bite),
p=10, inner face size 5. Monochromatic `σ10 = 0^10` on the 10-up rim cannot coexist
with `σ5 = 00012` on the inner face.
- **branching, odd outer + two even inner faces:** e.g. `UDUDUDDUDUDDDDD` bite=(5,12),
p=5, faces `[4,4]`. `σ5` outer with monochromatic `σ4` on *both* inner faces is not
jointly realisable. Branching tiles: **1011/1022 threaded, 11 fail.**
So the clean regular conjecture is **false**. Crucially this refutation is exactly the
`R_T` **coupling** (Finding 1) asserting itself at scale: the regular family sets outer
and inner states *independently per size*, but `R_T` is not a product, so a large
monochromatic rim over-constrains the annular cycle and forbids the paired inner
necklace. The failure is not about branching per se — it hits large even rims (non-
branching) and small odd rims with two even children (branching) alike.
### What it means for strategy
The uniform "one state per size, everywhere" family was a **too-strong shortcut** —
much stronger than the chain-pigeonhole conjecture, which only needs *some* compatible
selection per chain with freedom to choose a *different* state at each interface. Its
failure costs a cheap constructive route, **not** the conjecture: pairwise overlap
still always holds. The load transfers to the genuine object — **per-interface
selection respecting `R_T` coupling**, i.e. composing `R_T` along chains/trees with
per-seam freedom rather than a global family.
## Finding 9 — `R_T` composition: the pigeonhole verified exhaustively for n ≤ 14
`kempe_rt_composition_probe.py` — the conjecture proper, with full per-interface
freedom. Two modeling facts were established first:
- **The seam is exactly the singleton down apexes** of one inner face. A bite apex's
edge has parent tread faces on *both* sides, so it is internal to the parent's tile
and shares no medial vertex with the child.
- **Necklace states are exact, not approximate**: a child tile attaches with free
dihedral placement, so its realisable outer-sequence set is dihedral-closed and
"necklace match ⟺ some aligned sequence match". (This is the paper's own Def. of
medial boundary state.)
Define `Ext(T)` = necklaces realisable on a subtree's outer seam by a compatible
Kempe-balanced selection: `Ext(T) = {o : ∃(o, i⃗) ∈ R_T^joint, i_j ∈ Ext(C_j)}`,
leaves = all-bite tiles (no singleton interfaces, degenerate inner boundaries). The
maps are monotone, so the **antichain of minimal reachable Ext sets per seam size**
decides whether ∅ is reachable. Relations are cached (`kempe_rt_relations_cache.json`).
Result over all no-length-3-boundary tiles with `n ≤ 14` (7750 tiles, 1966 distinct
relations, 149 leaf, **27 branching**):
- **∅ is NOT reachable.** Every tree assemblable from this universe — branching
included — admits a compatible Kempe-balanced selection. Since every real tire tree
whose treads have `n ≤ 14` and no separating triangles is such a tree, the
chain-pigeonhole conjecture is **verified exhaustively for that class**.
- **Composition saturates in 2 rounds.** The minimal antichains are already closed
under every tile map after one pass: restriction does *not* accumulate along chains
— it bottoms out immediately. This is exactly the structural behaviour the paper's
§6 programme hoped for ("restriction sets cannot remain mutually disjoint").
- **Restriction is real but bounded.** Tightest subtrees per seam size: m=4 forces
2 of 3 necklaces, **m=5 forces a single necklace `{00012}`**, m=7 forces 3 of 10,
m=8 forces 8 of 34, m=14 forces 133 of 7515. Yet no parent relation ever misses a
minimal set entirely.
- **The blocky states are always offered.** Every smallest minimal Ext set contains
the regular necklace of its size (`0^m` or `0^{m-2}12`-type). The regular family
failed (Finding 8) because a single tile sometimes cannot take blocky-in and
blocky-out *jointly* — but per-interface freedom routes around it, and the data
shows subtrees always keep the blocky option available downward.
Caveats: (i) terminal facial triangles (innermost treads ending on a face of `G`) are
not yet modelled — our leaves are only the degenerate-inner-boundary all-bite tiles;
adding 3-faces as terminal children with `Ext = {012}` is a small extension. (ii) The
universe is abstract: it is a *superset* of real trees (good for the positive verdict;
an abstract obstruction, had one appeared, would still have needed a realisability
check).
## Open threads
- **Terminal-3-face leaves.** Extend the fixpoint with facial-triangle termination
(parent tiles with one 3-singleton face allowed as terminal); rerun.
- **Push `max_n`.** n=15 adds ~1 hr of one-time classification to the cache; the
2-round saturation suggests verdicts stabilise quickly, but a deeper universe is
the only way an obstruction could still appear.
- **Why saturation?** The 2-round fixpoint convergence is the empirical shadow of a
provable statement: composing tire restriction relations stabilises after one
level. A proof of that, with the minimal antichains characterised, would *be* the
pigeonhole lemma for this class.
- **Structural lemma for the n=15 uniform failures** (why a monochromatic large rim
forbids the paired odd-inner necklace) — still open, now lower priority.
@@ -0,0 +1,358 @@
"""Check the medial annular-cycle almost-two-colour condition.
For each generated plane triangulation G and each requested level source:
1. Build the full medial graph M(G).
2. Find depth-component tire annular medial subgraphs.
3. Enumerate simple cycles in those annular subgraphs.
4. Search for a proper vertex 3-colouring of M(G) such that every
such cycle uses two colours except at at most one vertex.
Run with Sage, for example:
sage -python papers/medial_tire_decompositions_of_plane_triangulations/experiments/check_medial_annular_cycle_condition.py --n-min 4 --n-max 8
"""
from __future__ import annotations
import argparse
from collections import defaultdict, deque
from itertools import combinations
from typing import Any, Iterable, Iterator, Sequence, cast
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
from sage.graphs.graph_coloring import all_graph_colorings # type: ignore[attr-defined] # pylint: disable=no-name-in-module
Edge = tuple[Any, Any]
Coloring = dict[Edge, int]
Source = tuple[Any, ...]
def vertex_key(v: Any) -> str:
return repr(v)
def edge_key(u: Any, v: Any) -> Edge:
return (u, v) if vertex_key(u) <= vertex_key(v) else (v, u)
def is_induced_cycle(g: Graph, vertices: Sequence[Any]) -> bool:
if len(vertices) < 3:
return False
h = cast(Graph, g.subgraph(list(vertices)))
return h.is_connected() and h.num_edges() == len(vertices) and all(
h.degree(v) == 2 for v in h.vertices()
)
def induced_cycle_sources(g: Graph, max_size: int | None = None) -> Iterator[Source]:
vertices = sorted(g.vertices(), key=vertex_key)
upper = len(vertices) if max_size is None else min(max_size, len(vertices))
for k in range(3, upper + 1):
for subset in combinations(vertices, k):
if is_induced_cycle(g, subset):
yield tuple(subset)
def level_sources(g: Graph, mode: str, max_cycle_source_size: int | None) -> Iterator[Source]:
if mode in ("vertex", "all"):
for v in sorted(g.vertices(), key=vertex_key):
yield (v,)
if mode in ("cycle", "all"):
yield from induced_cycle_sources(g, max_cycle_source_size)
def distances_from_source(g: Graph, source: Source) -> dict[Any, int]:
if len(source) == 1:
return dict(g.shortest_path_lengths(source[0]))
distances = {v: 0 for v in source}
queue: deque[Any] = deque(source)
while queue:
v = queue.popleft()
for w in g.neighbor_iterator(v):
if w in distances:
continue
distances[w] = distances[v] + 1
queue.append(w)
return distances
def embedded_copy(g: Graph) -> Graph:
emb = cast(Graph, g.copy())
if not emb.is_planar(set_embedding=True):
raise ValueError("graph is not planar")
return emb
def medial_graph(g: Graph) -> Graph:
"""Build the full medial graph from the embedding rotation at vertices."""
emb = embedded_copy(g)
rotation = emb.get_embedding()
m = Graph()
medial_vertices = [edge_key(u, v) for u, v, _ in emb.edge_iterator()]
m.add_vertices(medial_vertices)
for v, neighbors in rotation.items():
if len(neighbors) < 2:
continue
n = len(neighbors)
for i in range(n):
e1 = edge_key(v, neighbors[i])
e2 = edge_key(v, neighbors[(i + 1) % n])
if e1 != e2:
m.add_edge(e1, e2)
return m
def face_vertices(face: Sequence[tuple[Any, Any]]) -> set[Any]:
out: set[Any] = set()
for u, v in face:
out.add(u)
out.add(v)
return out
def face_edges(face: Sequence[tuple[Any, Any]]) -> set[Edge]:
return {edge_key(u, v) for u, v in face}
def dual_components_by_depth(
g: Graph, source: Source
) -> list[tuple[int, list[int], set[Edge]]]:
"""Return (depth, face-indices, annular-edge-set) for each depth component."""
emb = embedded_copy(g)
distances = distances_from_source(emb, source)
faces = emb.faces()
f_vertices = [face_vertices(face) for face in faces]
f_edges = [face_edges(face) for face in faces]
depths = [min(distances[v] for v in verts) for verts in f_vertices]
edge_faces: dict[Edge, list[int]] = defaultdict(list)
for idx, edges in enumerate(f_edges):
for edge in edges:
edge_faces[edge].append(idx)
dual_adj: dict[int, set[int]] = defaultdict(set)
for incident in edge_faces.values():
for a in range(len(incident)):
for b in range(a + 1, len(incident)):
dual_adj[incident[a]].add(incident[b])
dual_adj[incident[b]].add(incident[a])
components = []
seen = [False] * len(faces)
for start in range(len(faces)):
if seen[start]:
continue
depth = depths[start]
comp = [start]
seen[start] = True
stack = [start]
while stack:
f = stack.pop()
for h in dual_adj[f]:
if not seen[h] and depths[h] == depth:
seen[h] = True
comp.append(h)
stack.append(h)
annular_edges: set[Edge] = set()
for f in comp:
for u, v in f_edges[f]:
if {distances[u], distances[v]} == {depth, depth + 1}:
annular_edges.add(edge_key(u, v))
if len(annular_edges) >= 3:
components.append((depth, comp, annular_edges))
return components
def simple_cycle_vertex_sets(g: Graph) -> set[frozenset[Any]]:
vertices = sorted(g.vertices(), key=repr)
index = {v: i for i, v in enumerate(vertices)}
cycles: set[frozenset[Any]] = set()
def dfs(start: Any, current: Any, path: list[Any], seen: set[Any]) -> None:
for nxt in g.neighbor_iterator(current):
if nxt == start:
if len(path) >= 3:
cycles.add(frozenset(path))
continue
if nxt in seen or index[nxt] <= index[start]:
continue
seen.add(nxt)
path.append(nxt)
dfs(start, nxt, path, seen)
path.pop()
seen.remove(nxt)
for start in vertices:
dfs(start, start, [start], {start})
return cycles
def annular_medial_cycles(g: Graph, source: Source) -> list[frozenset[Edge]]:
m = medial_graph(g)
cycles: list[frozenset[Edge]] = []
seen: set[frozenset[Edge]] = set()
for _depth, _faces, annular_edges in dual_components_by_depth(g, source):
sub = cast(Graph, m.subgraph(list(annular_edges)))
for cycle in simple_cycle_vertex_sets(sub):
typed = frozenset(cast(Iterable[Edge], cycle))
if typed not in seen:
seen.add(typed)
cycles.append(typed)
return cycles
def almost_two_coloured(cycle: frozenset[Edge], coloring: Coloring) -> bool:
counts = defaultdict(int)
for vertex in cycle:
counts[coloring[vertex]] += 1
return min(counts.get(c, 0) for c in range(3)) <= 1
def first_cycle_violation(
cycles: Sequence[frozenset[Edge]], coloring: Coloring
) -> frozenset[Edge] | None:
for cycle in cycles:
if not almost_two_coloured(cycle, coloring):
return cycle
return None
def color_counts(cycle: frozenset[Edge], coloring: Coloring) -> dict[int, int]:
counts = {0: 0, 1: 0, 2: 0}
for vertex in cycle:
counts[coloring[vertex]] += 1
return counts
def coloring_witness(
m: Graph,
cycles: Sequence[frozenset[Edge]],
max_colorings: int | None,
) -> tuple[Coloring | None, int, bool, frozenset[Edge] | None, Coloring | None]:
checked = 0
last_violation = None
for raw in all_graph_colorings(m, 3, vertex_color_dict=True):
coloring = cast(Coloring, raw)
checked += 1
violation = first_cycle_violation(cycles, coloring)
if violation is None:
return coloring, checked, True, None, None
last_violation = violation
if max_colorings is not None and checked >= max_colorings:
return None, checked, False, last_violation, coloring
return None, checked, True, last_violation, coloring
def source_label(source: Source) -> str:
if len(source) == 1:
return f"vertex:{source[0]}"
return "cycle:{" + ",".join(map(str, source)) + "}"
def graphs_to_check(n: int, max_graphs: int | None):
for idx, g in enumerate(graphs.triangulations(n)):
if max_graphs is not None and idx >= max_graphs:
break
yield idx, cast(Graph, g)
def run(args: argparse.Namespace) -> None:
total_cases = 0
skipped_no_cycles = 0
witnesses = 0
failures = []
inconclusive = []
for n in range(args.n_min, args.n_max + 1):
print(f"n={n}")
for graph_idx, g in graphs_to_check(n, args.max_graphs_per_n):
m = medial_graph(g)
sources = list(level_sources(g, args.sources, args.max_cycle_source_size))
if args.max_sources_per_graph is not None:
sources = sources[: args.max_sources_per_graph]
for source in sources:
cycles = annular_medial_cycles(g, source)
if not cycles:
skipped_no_cycles += 1
continue
total_cases += 1
witness, checked, exhausted, violation, last_coloring = coloring_witness(
m, cycles, args.max_colorings
)
if witness is not None:
witnesses += 1
if args.verbose:
print(
f" graph={graph_idx} source={source_label(source)} "
f"cycles={len(cycles)} witness_after={checked}"
)
continue
record = {
"n": n,
"graph_idx": graph_idx,
"graph_edges": sorted(edge_key(u, v) for u, v, _ in g.edge_iterator()),
"source": source_label(source),
"cycles": len(cycles),
"checked": checked,
"exhausted": exhausted,
"violation_size": len(violation) if violation else None,
}
if args.failure_details and violation is not None and last_coloring is not None:
record["violation_cycle"] = sorted(violation)
record["violation_counts"] = color_counts(violation, last_coloring)
record["violation_coloring"] = {
edge: last_coloring[edge] for edge in sorted(violation)
}
if exhausted:
failures.append(record)
print(" FAILURE", record)
if args.stop_on_failure:
print_summary(total_cases, skipped_no_cycles, witnesses, failures, inconclusive)
return
else:
inconclusive.append(record)
print(" INCONCLUSIVE", record)
print_summary(total_cases, skipped_no_cycles, witnesses, failures, inconclusive)
def print_summary(
total_cases: int,
skipped_no_cycles: int,
witnesses: int,
failures: Sequence[dict],
inconclusive: Sequence[dict],
) -> None:
print()
print("summary")
print(f" checked source decompositions with annular cycles: {total_cases}")
print(f" skipped source decompositions with no annular cycles: {skipped_no_cycles}")
print(f" witnesses found: {witnesses}")
print(f" failures: {len(failures)}")
print(f" inconclusive: {len(inconclusive)}")
if failures:
print(f" first failure: {failures[0]}")
if inconclusive:
print(f" first inconclusive: {inconclusive[0]}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--n-min", type=int, default=4)
parser.add_argument("--n-max", type=int, default=8)
parser.add_argument("--sources", choices=("vertex", "cycle", "all"), default="vertex")
parser.add_argument("--max-cycle-source-size", type=int, default=6)
parser.add_argument("--max-graphs-per-n", type=int)
parser.add_argument("--max-sources-per-graph", type=int)
parser.add_argument("--max-colorings", type=int)
parser.add_argument("--stop-on-failure", action="store_true")
parser.add_argument("--failure-details", action="store_true")
parser.add_argument("--verbose", action="store_true")
run(parser.parse_args())
if __name__ == "__main__":
main()
@@ -0,0 +1,181 @@
"""Probe the parity mechanism behind Remark 5.8.
Claim X: in a proper 3-colouring of the 4-regular medial graph M(G) of a plane
triangulation G, for every face f of M(G) and every colour pair P = {a,b}, the
number of vertices on the boundary of f coloured a or b is even.
M(G) has two kinds of faces: a "vertex-face" per vertex v of G (the cyclic
sequence of edges around v) and a "face-face" per triangular face of G (its
three edges). The face-faces are triangles, trivially even (count 2); the
vertex-faces are the non-obvious case.
We build M(G) from a planar embedding's rotation system, enumerate proper
3-colourings of M(G), and check Claim X on every face / pair.
"""
from __future__ import annotations
import itertools
import networkx as nx
# --- a handful of small plane triangulations (maximal planar graphs) ---------
def tetrahedron() -> nx.Graph:
return nx.complete_graph(4)
def octahedron() -> nx.Graph:
# K_{2,2,2}: antipodal pairs non-adjacent
g = nx.Graph()
pairs = [(0, 1), (2, 3), (4, 5)]
nonadj = set(map(frozenset, pairs))
for u in range(6):
for v in range(u + 1, 6):
if frozenset((u, v)) not in nonadj:
g.add_edge(u, v)
return g
def stacked(levels: int) -> nx.Graph:
"""Apollonian-style: repeatedly insert a vertex in a triangular face."""
g = nx.Graph()
g.add_edges_from([(0, 1), (1, 2), (0, 2)])
faces = [(0, 1, 2)]
nxt = 3
for _ in range(levels):
a, b, c = faces.pop(0)
v = nxt
nxt += 1
g.add_edges_from([(v, a), (v, b), (v, c)])
faces += [(a, b, v), (b, c, v), (a, c, v)]
return g
def icosahedron() -> nx.Graph:
return nx.icosahedral_graph()
def double_wheel(rim: int) -> nx.Graph:
"""Two apexes over a rim cycle: a simple triangulated 'tire' with caps."""
g = nx.Graph()
g.add_cycle = None
for i in range(rim):
g.add_edge(i, (i + 1) % rim)
g.add_edge(i, "N")
g.add_edge(i, "S")
return g
# --- medial graph from a rotation system -------------------------------------
def rotation_system(g: nx.Graph) -> dict:
ok, emb = nx.check_planarity(g)
if not ok:
raise ValueError("graph is not planar")
return {v: list(emb.neighbors_cw_order(v)) for v in g.nodes()}, emb
def medial_graph(g: nx.Graph):
"""Return (M, vertex_faces, face_faces) built from the rotation system.
Medial vertices are edges of g (as sorted tuples). Around each vertex the
incident edges form a face cycle (vertex-face); around each triangular face
of g its three edges form a face cycle (face-face).
"""
rot, emb = rotation_system(g)
def ekey(u, v):
return (u, v) if u <= v else (v, u)
M = nx.Graph()
M.add_nodes_from(ekey(u, v) for u, v in g.edges())
vertex_faces = []
for v, order in rot.items():
edges = [ekey(v, w) for w in order]
vertex_faces.append(edges)
for i in range(len(edges)):
M.add_edge(edges[i], edges[(i + 1) % len(edges)])
# face-faces: traverse each face of the embedding once
seen = set()
face_faces = []
for u, v in list(emb.edges()):
if (u, v) in seen:
continue
face = emb.traverse_face(u, v, mark_half_edges=seen)
edges = [ekey(face[i], face[(i + 1) % len(face)]) for i in range(len(face))]
face_faces.append(edges)
return M, vertex_faces, face_faces
# --- proper 3-colourings of M(G) ---------------------------------------------
def proper_3_colorings(M: nx.Graph, limit: int | None = None):
nodes = list(M.nodes())
adj = {v: set(M.neighbors(v)) for v in nodes}
coloring: dict = {}
out = []
def rec(i):
if limit is not None and len(out) >= limit:
return
if i == len(nodes):
out.append(dict(coloring))
return
v = nodes[i]
used = {coloring[w] for w in adj[v] if w in coloring}
for c in (0, 1, 2):
if c not in used:
coloring[v] = c
rec(i + 1)
del coloring[v]
rec(0)
return out
def check_claim_x(name: str, g: nx.Graph, color_limit: int = 200):
M, vfaces, ffaces = medial_graph(g)
colorings = proper_3_colorings(M, limit=color_limit)
if not colorings:
print(f"{name}: M(G) has no proper 3-colouring (skip)")
return
faces = [("vertex", f) for f in vfaces] + [("face", f) for f in ffaces]
violations = 0
odd_vertex_faces = 0
for col in colorings:
for kind, face in faces:
for pair in ((0, 1), (0, 2), (1, 2)):
cnt = sum(1 for v in face if col[v] in pair)
if cnt % 2 != 0:
violations += 1
if kind == "vertex":
odd_vertex_faces += 1
deg = sorted({len(f) for f in vfaces})
print(f"{name}: |V(G)|={g.number_of_nodes()} |M|={M.number_of_nodes()} "
f"colourings tested={len(colorings)} vertex-face sizes={deg}")
print(f" Claim X violations: {violations} "
f"(vertex-face violations: {odd_vertex_faces})")
def main():
cases = [
("tetrahedron", tetrahedron()),
("octahedron", octahedron()),
("stacked-3", stacked(3)),
("stacked-6", stacked(6)),
("double_wheel-5", double_wheel(5)),
("double_wheel-6", double_wheel(6)),
("double_wheel-7", double_wheel(7)),
("icosahedron", icosahedron()),
]
for name, g in cases:
# ensure it is a triangulation (every face a triangle)
check_claim_x(name, g)
if __name__ == "__main__":
main()
@@ -0,0 +1,116 @@
"""Directly test Remark 5.8 on a genuine tire piece that contains a BITE.
A bite arises when the inner outerplanar graph O has a bridge: the bridge edge
is traversed twice by the outer-face walk, so it borders two tread triangles and
its medial vertex is adjacent to four annular medial vertices.
Minimal construction. Outer 4-cycle o0,o1,o2,o3; two interior vertices u,w
joined by a bridge u-w (V_in = {u,w}). Triangulate the disk so that u-w lies in
two tread triangles:
(o0,o1,u) (o0,u,o3) (o1,w,u) (o1,o2,w) (o2,o3,w) (o3,u,w)
Cap the outer cycle with an apex N (the bridge bounds no inner hole, so no inner
cap is needed). The result G is a closed plane triangulation; M(G) is 4-regular.
Edge classification (by endpoints): annular = one endpoint outer & one inner;
up tooth = both endpoints outer (outer-cycle edge); down tooth = both endpoints
inner (here only the bridge u-w). The bridge's medial vertex is the bite apex.
Remark 5.8 predicts every proper 3-colouring of M(G) restricts to a
Kempe-balanced colouring. Here the only non-trivial condition is the outer face
(the four up apexes), since the single bite contributes no singleton down teeth.
"""
from __future__ import annotations
import networkx as nx
from check_remark58_bitefree import ekey, medial_graph, proper_3_colorings
PAIRS = ((0, 1), (0, 2), (1, 2))
OUTER = ["o0", "o1", "o2", "o3"]
INNER = ["u", "w"]
TREAD_TRIANGLES = [
("o0", "o1", "u"),
("o0", "u", "o3"),
("o1", "w", "u"),
("o1", "o2", "w"),
("o2", "o3", "w"),
("o3", "u", "w"),
]
def build():
g = nx.Graph()
for tri in TREAD_TRIANGLES:
a, b, c = tri
g.add_edges_from([(a, b), (b, c), (a, c)])
# outer cap
for i in range(4):
g.add_edges_from([("N", OUTER[i]), ("N", OUTER[(i + 1) % 4])])
return g
def classify_tread_edges(g):
out = set(OUTER)
inn = set(INNER)
tread_edges = set()
for tri in TREAD_TRIANGLES:
a, b, c = tri
tread_edges |= {ekey(a, b), ekey(b, c), ekey(a, c)}
annular, up, down = [], [], []
for e in tread_edges:
a, b = e
ao, bo = a in out, b in out
ai, bi = a in inn, b in inn
if (ao and bi) or (ai and bo):
annular.append(e)
elif ao and bo:
up.append(e)
elif ai and bi:
down.append(e)
return annular, up, down
def run():
g = build()
assert nx.check_planarity(g)[0]
M = medial_graph(g)
annular, up, down = classify_tread_edges(g)
annular_set = set(annular)
# confirm there is a bite: a down edge whose medial vertex has 4 annular nbrs
bites = [e for e in down if sum(1 for nb in M.neighbors(e) if nb in annular_set) == 4]
print(f"tread: annular={len(annular)} up={len(up)} down={len(down)} "
f"bite apexes={len(bites)} (bite edge: {bites})")
colorings = proper_3_colorings(M, limit=20000)
balanced = 0
bad = []
for col in colorings:
ok = all(
sum(1 for e in up if col[e] in pair) % 2 == 0
for pair in PAIRS
)
if ok:
balanced += 1
else:
bad.append(col)
print(f"|V(G)|={g.number_of_nodes()} |M(G)|={M.number_of_nodes()} "
f"colourings tested={len(colorings)}")
print(f" outer-face (up-apex) balanced={balanced} UNBALANCED={len(bad)}")
if bad:
print(f" first unbalanced up colours: {[bad[0][e] for e in up]}")
print()
print("Remark 5.8 holds on this bite tread"
if not bad else
"Remark 5.8 FAILS on this bite tread")
return len(bad)
if __name__ == "__main__":
run()
@@ -0,0 +1,132 @@
"""Test Remark 5.8 on a bite tread that also has singleton down teeth in the
bite's inner-gap face -- the subtle case of the condition.
Inner outerplanar graph O = triangle (a,b,c) plus a pendant bridge a-d. Its
outer-face walk is the cyclic sequence W = [d, a, b, c, a]: the bridge a-d is
traversed twice (-> a bite), the triangle edges a-b, b-c, c-a once each (-> three
singleton down teeth, all sitting in the bite's inner-gap face).
We triangulate the annulus between an outer m-cycle and W by the lattice-path
method, searching interleavings for one giving a simple closed triangulation
after capping the outer cycle with an apex N. Then we test Remark 5.8: every
proper 3-colouring of M(G) restricts to a Kempe-balanced colouring, i.e.
* the up apexes (outer edges) are even per colour pair, and
* the three singleton down apexes (a-b, b-c, c-a), which share the bite-gap
face, are even per colour pair (equivalently: a rainbow).
"""
from __future__ import annotations
import itertools
import networkx as nx
from check_remark58_bitefree import ekey, medial_graph, proper_3_colorings
PAIRS = ((0, 1), (0, 2), (1, 2))
INNER_WALK = ["d", "a", "b", "c", "a"] # bridge a-d traversed twice
SINGLETON_DOWN = [ekey("a", "b"), ekey("b", "c"), ekey("c", "a")]
BITE_EDGE = ekey("a", "d")
def build_tread(m: int, path: str):
"""Build the annular triangulation for a given lattice path (m 'O', L 'I')."""
outer = [f"o{t}" for t in range(m)]
W = INNER_WALK
L = len(W)
g = nx.Graph()
g.add_edge(outer[0], W[0]) # anchor
i = j = 0
tread_triangles = []
for mv in path:
if mv == "O":
tri = (outer[i % m], W[j % L], outer[(i + 1) % m])
i += 1
else:
tri = (outer[i % m], W[j % L], W[(j + 1) % L])
j += 1
a, b, c = tri
g.add_edges_from([(a, b), (b, c), (a, c)])
tread_triangles.append(tri)
if (i, j) != (m, L):
return None
return g, outer, tread_triangles
def cap_and_validate(g, outer):
"""Cap the outer cycle with apex N; require a simple closed triangulation."""
h = g.copy()
for t in range(len(outer)):
h.add_edges_from([("N", outer[t]), ("N", outer[(t + 1) % len(outer)])])
if not nx.check_planarity(h)[0]:
return None
V, E = h.number_of_nodes(), h.number_of_edges()
if E != 3 * V - 6: # maximal planar == triangulation
return None
return h
def find_construction(m: int):
L = len(INNER_WALK)
for combo in itertools.combinations(range(m + L), L):
path = "".join("I" if t in combo else "O" for t in range(m + L))
built = build_tread(m, path)
if built is None:
continue
g, outer, tris = built
h = cap_and_validate(g, outer)
if h is not None:
return h, outer, tris, path
return None
def run():
for m in (4, 5, 6, 7):
found = find_construction(m)
if found:
break
if not found:
print("no valid bite-with-singletons triangulation found")
return 1
h, outer, tris, path = found
M = medial_graph(h)
annular = set()
for tri in tris:
a, b, c = tri
for e in (ekey(a, b), ekey(b, c), ekey(a, c)):
x, y = e
xo, yo = x in outer, y in outer
if (xo and not yo) or (yo and not xo):
annular.add(e)
n_bite_nbrs = sum(1 for nb in M.neighbors(BITE_EDGE) if nb in annular)
up = [ekey(outer[t], outer[(t + 1) % len(outer)]) for t in range(len(outer))]
up = [e for e in up if e in M]
print(f"m={len(outer)} path={path} |V(G)|={h.number_of_nodes()} "
f"|M(G)|={M.number_of_nodes()}")
print(f"bite edge {BITE_EDGE}: annular neighbours={n_bite_nbrs} (4 => bite)")
print(f"up apexes={len(up)} singleton down apexes={SINGLETON_DOWN}")
colorings = proper_3_colorings(M, limit=50000)
bad_outer = bad_bitegap = 0
for col in colorings:
if any(sum(1 for e in up if col[e] in p) % 2 for p in PAIRS):
bad_outer += 1
if any(sum(1 for e in SINGLETON_DOWN if col[e] in p) % 2 for p in PAIRS):
bad_bitegap += 1
print(f"colourings tested={len(colorings)}")
print(f" outer face unbalanced: {bad_outer}")
print(f" bite-gap face (3 singletons) unbalanced: {bad_bitegap}")
print()
ok = (bad_outer == 0 and bad_bitegap == 0)
print("Remark 5.8 holds on this bite-with-singletons tread"
if ok else "Remark 5.8 FAILS on this bite-with-singletons tread")
return 0 if ok else 1
if __name__ == "__main__":
run()
@@ -0,0 +1,150 @@
"""Directly test Remark 5.8 on genuine (bite-free) tire pieces.
Construction. Build a triangulated annulus (an antiprism band) between an outer
p-cycle O = o_0..o_{p-1} and an inner p-cycle I = i_0..i_{p-1}, with the 2p
triangles
(o_k, o_{k+1}, i_k) and (o_{k+1}, i_k, i_{k+1}).
Cap the outer disk with an apex N joined to all o_k and the inner disk with an
apex S joined to all i_k. The result G is a closed plane triangulation, so its
medial graph M(G) is 4-regular.
The tread T is the annulus; its full medial tire graph M(T) is the subgraph of
M(G) on the medial vertices of the tread edges (outer, inner and annular edges).
This tread has simple boundaries, hence no bites: the up teeth are the outer
edges, the down teeth the inner edges, and the only valid faces are the outer
face (up apexes) and the root face (down apexes).
Remark 5.8 predicts: every proper 3-colouring of M(G), restricted to M(T), is
Kempe-balanced, i.e. for each colour pair P the up apexes coloured in P are even
in number, and likewise the down apexes. We enumerate colourings of M(G) and
check this.
"""
from __future__ import annotations
import itertools
import networkx as nx
PAIRS = ((0, 1), (0, 2), (1, 2))
def ekey(u, v):
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
def build_capped_annulus(p: int):
g = nx.Graph()
O = [("o", k) for k in range(p)]
I = [("i", k) for k in range(p)]
outer_edges, inner_edges, annular_edges = [], [], []
for k in range(p):
o, on = O[k], O[(k + 1) % p]
i, ino = I[k], I[(k + 1) % p]
outer_edges.append(ekey(o, on))
inner_edges.append(ekey(i, ino))
annular_edges += [ekey(o, i), ekey(on, i)]
# tread triangles
g.add_edges_from([(o, on), (on, i), (o, i)]) # (o_k,o_{k+1},i_k)
g.add_edges_from([(on, i), (i, ino), (on, ino)]) # (o_{k+1},i_k,i_{k+1})
# caps
for k in range(p):
g.add_edges_from([("N", O[k]), ("N", O[(k + 1) % p])])
g.add_edges_from([("S", I[k]), ("S", I[(k + 1) % p])])
meta = {
"outer_edges": [ekey(*e) for e in outer_edges],
"inner_edges": [ekey(*e) for e in inner_edges],
"annular_edges": [ekey(*e) for e in annular_edges],
}
return g, meta
def medial_graph(g: nx.Graph) -> nx.Graph:
ok, emb = nx.check_planarity(g)
if not ok:
raise ValueError("not planar")
M = nx.Graph()
M.add_nodes_from(ekey(u, v) for u, v in g.edges())
for v in g.nodes():
order = list(emb.neighbors_cw_order(v))
edges = [ekey(v, w) for w in order]
for a in range(len(edges)):
M.add_edge(edges[a], edges[(a + 1) % len(edges)])
return M
def proper_3_colorings(M: nx.Graph, limit: int):
nodes = list(M.nodes())
adj = {v: set(M.neighbors(v)) for v in nodes}
coloring: dict = {}
out = []
def rec(i):
if len(out) >= limit:
return
if i == len(nodes):
out.append(dict(coloring))
return
v = nodes[i]
used = {coloring[w] for w in adj[v] if w in coloring}
for c in (0, 1, 2):
if c not in used:
coloring[v] = c
rec(i + 1)
del coloring[v]
rec(0)
return out
def is_kempe_balanced(coloring, up_apexes, down_apexes):
for face in (up_apexes, down_apexes):
for pair in PAIRS:
if sum(1 for e in face if coloring[e] in pair) % 2 != 0:
return False, face is down_apexes
return True, None
def run(p: int, limit: int = 4000):
g, meta = build_capped_annulus(p)
M = medial_graph(g)
up = meta["outer_edges"]
down = meta["inner_edges"]
colorings = proper_3_colorings(M, limit)
balanced = 0
unbalanced = []
for col in colorings:
ok, _ = is_kempe_balanced(col, up, down)
if ok:
balanced += 1
else:
unbalanced.append(col)
n_ann = len(meta["annular_edges"])
print(f"p={p}: |V(G)|={g.number_of_nodes()} |M(G)|={M.number_of_nodes()} "
f"|A(T)|={n_ann} up={len(up)} down={len(down)}")
print(f" colourings tested={len(colorings)} (cap {limit}) "
f"balanced={balanced} UNBALANCED={len(unbalanced)}")
if unbalanced:
col = unbalanced[0]
upc = [col[e] for e in up]
dnc = [col[e] for e in down]
print(f" first unbalanced restriction: up colours={upc} down colours={dnc}")
return len(unbalanced)
def main():
total_bad = 0
for p in (3, 4, 5, 6):
total_bad += run(p)
print()
print("Remark 5.8 (bite-free) holds on all tested colourings"
if total_bad == 0 else
f"Remark 5.8 (bite-free) FAILS: {total_bad} unbalanced restrictions found")
if __name__ == "__main__":
main()
@@ -0,0 +1,350 @@
"""Compare full and reduced medial tire graphs on generated tires.
The new medial decomposition paper defines:
* full medial tire graph: the subgraph of M(G) induced by medial
vertices corresponding to edges incident to tread triangles;
* reduced medial tire graph: delete same-boundary medial edges and
chord-only medial edges.
For a tire tread inside an ambient triangulation, the medial edges
visible in the tread come from annular triangular faces. This script
checks whether any same-boundary medial edges are actually present in
that model. It also compares against the older standalone drawing
model, which added artificial outer/inner boundary faces.
"""
from __future__ import annotations
import argparse
import itertools
import random
from collections import Counter
Edge = tuple[int, int]
MedialEdge = tuple[Edge, Edge]
def random_tire(m: int, k: int, n_chords: int = 0, seed: int | None = None) -> dict:
"""Generate the same labelled annular tires used in earlier experiments."""
rng = random.Random(seed)
outer = list(range(m))
inner = list(range(m, m + k))
edges: set[Edge] = set()
for i in range(m):
edges.add(edge_key(outer[i], outer[(i + 1) % m]))
for j in range(k):
edges.add(edge_key(inner[j], inner[(j + 1) % k]))
inner_chords = set()
candidates = []
for a in range(k):
for b in range(a + 2, k):
if not (a == 0 and b == k - 1):
candidates.append((a, b))
rng.shuffle(candidates)
for a, b in candidates:
if len(inner_chords) >= n_chords:
break
if any((a < a2 < b < b2) or (a2 < a < b2 < b) for a2, b2 in inner_chords):
continue
inner_chords.add((a, b))
edges.add(edge_key(inner[a], inner[b]))
edges.add(edge_key(outer[0], inner[0]))
moves = ["O"] * m + ["I"] * k
rng.shuffle(moves)
triangles = []
i, j = 0, 0
for move in moves:
if move == "O":
tri = (outer[i % m], inner[j % k], outer[(i + 1) % m])
triangles.append(tri)
edges.add(edge_key(inner[j % k], outer[(i + 1) % m]))
i += 1
else:
tri = (outer[i % m], inner[j % k], inner[(j + 1) % k])
triangles.append(tri)
edges.add(edge_key(outer[i % m], inner[(j + 1) % k]))
j += 1
return {
"m": m,
"k": k,
"n_chords": len(inner_chords),
"outer": outer,
"inner": inner,
"edges": sorted(edges),
"triangles": triangles,
"inner_chords": sorted(inner_chords),
"lattice_path": "".join(moves),
"seed": seed,
}
def tire_from_path(m: int, k: int, chords: tuple[tuple[int, int], ...], path: str) -> dict:
outer = list(range(m))
inner = list(range(m, m + k))
edges: set[Edge] = set()
for i in range(m):
edges.add(edge_key(outer[i], outer[(i + 1) % m]))
for j in range(k):
edges.add(edge_key(inner[j], inner[(j + 1) % k]))
for a, b in chords:
edges.add(edge_key(inner[a], inner[b]))
edges.add(edge_key(outer[0], inner[0]))
triangles = []
i, j = 0, 0
for move in path:
if move == "O":
tri = (outer[i % m], inner[j % k], outer[(i + 1) % m])
triangles.append(tri)
edges.add(edge_key(inner[j % k], outer[(i + 1) % m]))
i += 1
else:
tri = (outer[i % m], inner[j % k], inner[(j + 1) % k])
triangles.append(tri)
edges.add(edge_key(outer[i % m], inner[(j + 1) % k]))
j += 1
return {
"m": m,
"k": k,
"n_chords": len(chords),
"outer": outer,
"inner": inner,
"edges": sorted(edges),
"triangles": triangles,
"inner_chords": sorted(chords),
"lattice_path": path,
"seed": None,
}
def chord_crosses(c1: tuple[int, int], c2: tuple[int, int]) -> bool:
a, b = c1
c, d = c2
return (a < c < b < d) or (c < a < d < b)
def chord_sets(k: int, max_chords: int) -> list[tuple[tuple[int, int], ...]]:
candidates = []
for a in range(k):
for b in range(a + 2, k):
if not (a == 0 and b == k - 1):
candidates.append((a, b))
out = [()]
def rec(start: int, chosen: tuple[tuple[int, int], ...]) -> None:
if len(chosen) >= max_chords:
return
for idx in range(start, len(candidates)):
chord = candidates[idx]
if any(chord_crosses(chord, old) for old in chosen):
continue
nxt = chosen + (chord,)
out.append(nxt)
rec(idx + 1, nxt)
rec(0, ())
return out
def lattice_paths(m: int, k: int):
for o_positions in itertools.combinations(range(m + k), m):
o_set = set(o_positions)
yield "".join("O" if idx in o_set else "I" for idx in range(m + k))
def edge_key(u: int, v: int) -> Edge:
return tuple(sorted((u, v)))
def face_edges(face: tuple[int, ...]) -> list[Edge]:
return [edge_key(face[i], face[(i + 1) % len(face)]) for i in range(len(face))]
def is_cycle_edge(edge: Edge, cycle: list[int]) -> bool:
cycle_set = set(cycle)
if not set(edge) <= cycle_set:
return False
n = len(cycle)
idx = {v: i for i, v in enumerate(cycle)}
a, b = idx[edge[0]], idx[edge[1]]
return (a - b) % n in (1, n - 1)
def is_inner_chord(edge: Edge, m: int, k: int) -> bool:
u, v = edge
if not (m <= u < m + k and m <= v < m + k):
return False
a, b = u - m, v - m
d = abs(a - b)
return min(d, k - d) != 1
def suppress_reason(e1: Edge, e2: Edge, tire: dict) -> str | None:
outer = tire["outer"]
inner = tire["inner"]
if is_cycle_edge(e1, outer) and is_cycle_edge(e2, outer):
return "outer_boundary"
if is_cycle_edge(e1, inner) and is_cycle_edge(e2, inner):
return "inner_boundary"
m, k = tire["m"], tire["k"]
if is_inner_chord(e1, m, k) or is_inner_chord(e2, m, k):
return "inner_chord"
return None
def medial_from_faces(faces: list[tuple[int, ...]], retained: set[Edge]) -> set[MedialEdge]:
medial_edges: set[MedialEdge] = set()
for face in faces:
boundary = [e for e in face_edges(face) if e in retained]
if len(boundary) < 2:
continue
for i, e in enumerate(boundary):
nxt = boundary[(i + 1) % len(boundary)]
if e != nxt:
medial_edges.add(tuple(sorted((e, nxt))))
return medial_edges
def compare_tire(tire: dict, *, standalone_boundary_faces: bool) -> dict:
annular_faces = [tuple(tri) for tri in tire["triangles"]]
faces = list(annular_faces)
if standalone_boundary_faces:
faces.append(tuple(tire["outer"]))
faces.append(tuple(reversed(tire["inner"])))
# Definition 3.1 includes edges incident to at least one tread triangle.
retained = {e for face in annular_faces for e in face_edges(face)}
full_edges = medial_from_faces(faces, retained)
removed = {me for me in full_edges if suppress_reason(me[0], me[1], tire)}
reduced_edges = full_edges - removed
reasons = Counter(suppress_reason(me[0], me[1], tire) for me in removed)
reasons.pop(None, None)
return {
"vertices": len(retained),
"full_edges": len(full_edges),
"reduced_edges": len(reduced_edges),
"removed": len(removed),
"reasons": reasons,
"examples": sorted(removed)[:5],
}
def run_sweep(args: argparse.Namespace) -> None:
ambient_cases = 0
ambient_differ = []
standalone_cases = 0
standalone_differ = []
ambient_reasons: Counter[str] = Counter()
standalone_reasons: Counter[str] = Counter()
max_chords = args.max_chords
for m in range(args.min_cycle, args.max_cycle + 1):
for k in range(args.min_cycle, args.max_cycle + 1):
for chords in range(max_chords + 1):
for seed in range(args.seeds):
tire = random_tire(m=m, k=k, n_chords=chords, seed=seed)
ambient = compare_tire(tire, standalone_boundary_faces=False)
ambient_cases += 1
ambient_reasons.update(ambient["reasons"])
if ambient["removed"]:
ambient_differ.append((m, k, chords, seed, tire, ambient))
standalone = compare_tire(tire, standalone_boundary_faces=True)
standalone_cases += 1
standalone_reasons.update(standalone["reasons"])
if standalone["removed"]:
standalone_differ.append((m, k, chords, seed, tire, standalone))
print("ambient tread-face model")
print(f" cases checked: {ambient_cases}")
print(f" cases where full != reduced: {len(ambient_differ)}")
print(f" removed-edge reasons: {dict(sorted(ambient_reasons.items()))}")
if ambient_differ:
m, k, chords, seed, tire, result = ambient_differ[0]
print(" first difference:")
print(f" m={m} k={k} requested_chords={chords} seed={seed}")
print(f" path={tire['lattice_path']} chords={tire['inner_chords']}")
print(f" removed examples={result['examples']}")
print()
print("standalone tire-with-boundary-faces model")
print(f" cases checked: {standalone_cases}")
print(f" cases where full != reduced: {len(standalone_differ)}")
print(f" removed-edge reasons: {dict(sorted(standalone_reasons.items()))}")
if standalone_differ:
m, k, chords, seed, tire, result = standalone_differ[0]
print(" first difference:")
print(f" m={m} k={k} requested_chords={chords} seed={seed}")
print(f" path={tire['lattice_path']} chords={tire['inner_chords']}")
print(f" full_edges={result['full_edges']} reduced_edges={result['reduced_edges']}")
print(f" removed examples={result['examples']}")
def run_exhaustive(args: argparse.Namespace) -> None:
ambient_cases = 0
ambient_differ = []
standalone_cases = 0
standalone_differ = []
for m in range(args.min_cycle, args.max_cycle + 1):
for k in range(args.min_cycle, args.max_cycle + 1):
for chords in chord_sets(k, args.max_chords):
for path in lattice_paths(m, k):
tire = tire_from_path(m, k, chords, path)
ambient = compare_tire(tire, standalone_boundary_faces=False)
ambient_cases += 1
if ambient["removed"]:
ambient_differ.append((m, k, chords, path, ambient))
standalone = compare_tire(tire, standalone_boundary_faces=True)
standalone_cases += 1
if standalone["removed"]:
standalone_differ.append((m, k, chords, path, standalone))
print("exhaustive ambient tread-face model")
print(f" cases checked: {ambient_cases}")
print(f" cases where full != reduced: {len(ambient_differ)}")
if ambient_differ:
m, k, chords, path, result = ambient_differ[0]
print(" first difference:")
print(f" m={m} k={k} chords={chords} path={path}")
print(f" removed examples={result['examples']}")
print()
print("exhaustive standalone tire-with-boundary-faces model")
print(f" cases checked: {standalone_cases}")
print(f" cases where full != reduced: {len(standalone_differ)}")
if standalone_differ:
m, k, chords, path, result = standalone_differ[0]
print(" first difference:")
print(f" m={m} k={k} chords={chords} path={path}")
print(f" full_edges={result['full_edges']} reduced_edges={result['reduced_edges']}")
print(f" removed examples={result['examples']}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--min-cycle", type=int, default=3)
parser.add_argument("--max-cycle", type=int, default=8)
parser.add_argument("--max-chords", type=int, default=3)
parser.add_argument("--seeds", type=int, default=50)
parser.add_argument("--exhaustive", action="store_true")
args = parser.parse_args()
if args.exhaustive:
run_exhaustive(args)
else:
run_sweep(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,144 @@
"""Picture: evening a terminal (leaf) triangle by the two-vertex operation:
add y at the midpoint of uv and z at the centroid of uvt, delete uv, add edges
xy, uy, vy, zy, zu, zv, zt. The leaf becomes a 4-wheel tread with hub z.
Panels:
A before: terminal face uvt, level cycle C_k = the 3-cycle (odd seam)
B after: seam u-y-v-t (length 4, even); leaf = 4-wheel with hub z (level k+1)
C medial overlay with the canonical colouring: seam apexes mono-3,
leaf annular 4-cycle alternating 1,2 -- proper, no ears, no defect.
"""
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
HERE = os.path.dirname(os.path.abspath(__file__))
PAL = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # colours "1","2","3"
GRAY = "#999999"
u, v, t, x = (-1.0, 0.0), (1.0, 0.0), (0.0, -1.6), (0.0, 1.0)
y = (0.0, 0.0) # midpoint of uv
z = (0.0, -1.6 / 3) # centroid of uvt
def mid(a, b):
return ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
def vertex(ax, p, name, dx, dy, color="black"):
ax.plot(*p, "o", color=color, ms=5.5, zorder=6)
ax.annotate(name, p, textcoords="offset points", xytext=(dx, dy),
fontsize=10, fontweight="bold", zorder=6)
def panel_before(ax):
ax.fill([u[0], v[0], t[0]], [u[1], v[1], t[1]], color="#fde9d9")
for a, b in [(u, v), (v, t), (t, u)]:
ax.plot([a[0], b[0]], [a[1], b[1]], color="black", lw=2.4)
for p in (u, v):
ax.plot([x[0], p[0]], [x[1], p[1]], color=GRAY, lw=0.9)
ax.annotate("terminal face\n(leaf of tire tree)", (0, -0.62), ha="center",
fontsize=8, color="#b06030")
vertex(ax, u, "u", -12, -2); vertex(ax, v, "v", 8, -2)
vertex(ax, t, "t", 0, -14); vertex(ax, x, "x", 8, 2)
ax.annotate("(apex in tread above)", x, textcoords="offset points",
xytext=(16, -2), fontsize=7, color=GRAY)
def panel_after(ax, faint=1.0):
# wheel faces
for tri, c in [((u, y, z), "#fde9d9"), ((y, v, z), "#fdf3d9"),
((v, t, z), "#fde9d9"), ((t, u, z), "#fdf3d9")]:
ax.fill([p[0] for p in tri], [p[1] for p in tri], color=c, alpha=faint)
# seam (level cycle) bold: u-y, y-v, v-t, t-u
for a, b in [(u, y), (y, v), (v, t), (t, u)]:
ax.plot([a[0], b[0]], [a[1], b[1]], color="black", lw=2.4, alpha=faint)
# parent spokes xu, xy, xv
for p in (u, y, v):
ax.plot([x[0], p[0]], [x[1], p[1]], color=GRAY, lw=0.9, alpha=faint)
# leaf spokes zu, zy, zv, zt
for p in (u, y, v, t):
ax.plot([z[0], p[0]], [z[1], p[1]], color="black", lw=1.0, alpha=faint)
vertex(ax, u, "u", -12, -2); vertex(ax, v, "v", 8, -2)
vertex(ax, t, "t", 0, -14); vertex(ax, x, "x", 8, 2)
vertex(ax, y, "y", 6, 6); vertex(ax, z, "z", 7, -4)
def panel_medial(ax):
panel_after(ax, faint=0.35)
apexes = {"uy": mid(u, y), "yv": mid(y, v), "vt": mid(v, t), "tu": mid(t, u)}
leaf_ann = {"zu": mid(z, u), "zy": mid(z, y), "zv": mid(z, v), "zt": mid(z, t)}
par_ann = {"ux": mid(u, x), "xy": mid(x, y), "xv": mid(x, v)}
# leaf annular 4-cycle (faces uyz, yvz, vtz, tuz)
ring = ["zu", "zy", "zv", "zt"]
for i in range(4):
a, b = leaf_ann[ring[i]], leaf_ann[ring[(i + 1) % 4]]
ax.plot([a[0], b[0]], [a[1], b[1]], color="#555555", lw=1.6, zorder=4)
# parent annular path m_ux - m_xy - m_xv (faces xuy, xyv)
for a, b in [("ux", "xy"), ("xy", "xv")]:
ax.plot([par_ann[a][0], par_ann[b][0]], [par_ann[a][1], par_ann[b][1]],
color="#555555", lw=1.6, zorder=4)
# apex spokes: each seam apex to its two leaf-annular and two parent nbrs
spokes = [("uy", "zu"), ("uy", "zy"), ("yv", "zy"), ("yv", "zv"),
("vt", "zv"), ("vt", "zt"), ("tu", "zt"), ("tu", "zu")]
for a, b in spokes:
pa, pb = apexes[a], leaf_ann[b]
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#aaaaaa", lw=1.0, zorder=3)
for a, b in [("uy", "ux"), ("uy", "xy"), ("yv", "xy"), ("yv", "xv")]:
pa, pb = apexes[a], par_ann[b]
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#aaaaaa", lw=1.0, zorder=3)
# off-picture parent stubs for m_vt, m_tu
for k, d in [("vt", (0.25, -0.18)), ("tu", (-0.25, -0.18))]:
p = apexes[k]
ax.plot([p[0], p[0] + d[0]], [p[1], p[1] + d[1]], color="#cccccc",
lw=0.9, linestyle=":", zorder=2)
# colours: apexes mono-3 (green); leaf ring alternating 1,2; parent 1,2,1
col = {}
for k in apexes: col[("a", k)] = 2
for k, c in zip(ring, (0, 1, 0, 1)): col[("l", k)] = c
for k, c in zip(("ux", "xy", "xv"), (0, 1, 0)): col[("p", k)] = c
for k, p in apexes.items():
ax.plot(*p, "o", color=PAL[2], ms=11, markeredgecolor="black", zorder=5)
for k, p in leaf_ann.items():
ax.plot(*p, "o", color=PAL[col[("l", k)]], ms=9,
markeredgecolor="black", zorder=5)
for k, p in par_ann.items():
ax.plot(*p, "o", color=PAL[col[("p", k)]], ms=9,
markeredgecolor="black", zorder=5)
lbl = {"uy": (-26, 4), "yv": (12, 4), "vt": (12, -2), "tu": (-30, -2)}
for k, p in apexes.items():
ax.annotate(f"m_{k}", p, textcoords="offset points", xytext=lbl[k],
fontsize=7, zorder=6)
fig, axes = plt.subplots(1, 3, figsize=(14, 4.8))
for ax in axes:
ax.set_xlim(-1.7, 1.7)
ax.set_ylim(-2.1, 1.45)
ax.set_aspect("equal")
ax.axis("off")
panel_before(axes[0])
axes[0].set_title("A. before: leaf = terminal face uvt\nseam C_k = 3-cycle (odd)",
fontsize=9)
panel_after(axes[1])
axes[1].set_title("B. add y = mid(uv), z = centroid; delete uv;\n"
"add xy, uy, vy, zy, zu, zv, zt\n"
"seam u-y-v-t (even); leaf = 4-wheel, hub z (level k+1)",
fontsize=9)
panel_medial(axes[2])
axes[2].set_title("C. medial + canonical colouring:\nseam apexes all 3 (green), "
"leaf ring alternates 1,2 — proper, no ears", fontsize=9)
fig.suptitle(
"Evening a terminal leaf with the two-vertex operation (y splits uv under x; "
"z stellates the leaf as a wheel hub).\n"
"Both new vertices have degree 4; the leaf tread is a 4-wheel with an even "
"annular cycle, so the monochromatic-3 seam is VALID — no leaf defect.",
fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.86))
out = os.path.join(HERE, "evened_leaf.png")
fig.savefig(out, dpi=170)
print("wrote", out)
@@ -0,0 +1,145 @@
"""Straight-line planar drawing of the minimal genuine obstruction found by the
even-level-cycle programme: the ring triangulation sizes=[3,6,3], leaf='face'
(generator random.Random(2), the 27th graph), 12 vertices. It survives
exhausting insertion sites x tread phases x root colour-orders (residue_phase_
sweep.py: 24 settings, 0 ok) and fails at the leaf-gadget removal step.
Embedding: networkx planar_layout (a canonical-ordering straight-line embedding
of a planar graph), recentred. We additionally VERIFY no two non-incident edges
cross before drawing. Every triangulation on >=4 vertices is 3-connected, so a
crossing-free straight-line embedding is guaranteed to exist.
"""
import os, random
import numpy as np
import networkx as nx
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import kempe_even_program_harness as H
HERE = os.path.dirname(os.path.abspath(__file__))
LEVCOL = {0: "#d9d9d9", 1: "#9ecae1", 2: "#fc9272"} # by BFS level
def reconstruct(seed, idx):
rng = random.Random(seed)
for _ in range(idx + 1):
sizes, leaf = H.random_profile(rng)
g, outer = H.ring_triangulation(sizes, leaf, rng)
return g, outer
def planar_pos(g):
nxg = nx.Graph()
nxg.add_nodes_from(g.rot)
for ed in g.edges():
a, b = tuple(ed); nxg.add_edge(a, b)
ok, _ = nx.check_planarity(nxg)
assert ok, "graph is not planar?!"
pos = nx.planar_layout(nxg)
pts = np.array([pos[v] for v in g.rot])
c = pts.mean(axis=0); s = np.abs(pts - c).max()
return {v: ((pos[v][0] - c[0]) / s, (pos[v][1] - c[1]) / s) for v in g.rot}
def seg_cross(p, q, r, s):
def o(a, b, c):
return (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0])
d1, d2, d3, d4 = o(r, s, p), o(r, s, q), o(p, q, r), o(p, q, s)
return ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0))
def verify_planar(g, pos):
edges = [tuple(e) for e in g.edges()]
bad = []
for i in range(len(edges)):
a, b = edges[i]
for j in range(i + 1, len(edges)):
c, d = edges[j]
if len({a, b, c, d}) < 4:
continue
if seg_cross(pos[a], pos[b], pos[c], pos[d]):
bad.append((edges[i], edges[j]))
return bad
g, outer = reconstruct(2, 26)
g.check()
an = H.Analysis(g.copy(), outer)
pos = planar_pos(g)
bad = verify_planar(g, pos)
print("crossing edge-pairs:", bad if bad else "NONE -- valid straight-line planar embedding")
assert not bad, "embedding has crossings"
terminal = tuple(an.terminal[0])
odd_seam = [c for k, c in an.seams if len(c) % 2][0]
faces = [tuple(f) for f in g.faces()]
outer_set = frozenset(outer)
fig, axes = plt.subplots(1, 2, figsize=(13.5, 6.8))
xs = [p[0] for p in pos.values()]; ys = [p[1] for p in pos.values()]
mx = max(abs(min(xs)), abs(max(xs))); my = max(abs(min(ys)), abs(max(ys)))
for ax in axes:
ax.set_aspect("equal"); ax.axis("off")
ax.set_xlim(-mx - 0.25, mx + 0.25); ax.set_ylim(-my - 0.25, my + 0.35)
def draw_faces(ax):
for f in faces:
if frozenset(f) == outer_set:
continue
ax.add_patch(Polygon([pos[v] for v in f], closed=True,
facecolor="#fbfbfb", edgecolor="none", zorder=0))
def draw_edges(ax, bold=None):
bold = bold or set()
for ed in g.edges():
a, b = tuple(ed)
hot = frozenset((a, b)) in bold
ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]],
color="#cc2222" if hot else "#7a7a7a",
lw=2.8 if hot else 1.1, zorder=2)
def draw_verts(ax, by_level=False):
for v, p in pos.items():
fc = LEVCOL[an.level[v]] if by_level else "white"
ax.plot(*p, "o", ms=20, mfc=fc, mec="#222222", mew=1.6, zorder=4)
ax.annotate(str(v), p, ha="center", va="center", fontsize=10,
fontweight="bold", zorder=5)
draw_faces(axes[0]); draw_edges(axes[0]); draw_verts(axes[0])
axes[0].set_title("A. ring [3,6,3] + face leaf, 12 vertices\n"
"straight-line planar embedding (verified crossing-free)",
fontsize=10)
draw_faces(axes[1])
seam_edges = {frozenset((odd_seam[i], odd_seam[(i+1) % len(odd_seam)]))
for i in range(len(odd_seam))}
axes[1].add_patch(Polygon([pos[v] for v in terminal], closed=True,
facecolor="#fee0d2", edgecolor="none", zorder=1))
draw_edges(axes[1], bold=seam_edges)
draw_verts(axes[1], by_level=True)
tc = np.mean([pos[v] for v in terminal], axis=0)
axes[1].annotate("terminal triangle " + "-".join(map(str, terminal)) +
"\n(level-2 odd seam; carries the leaf\ngadget whose removal "
"STILL FAILS)",
xy=tc, xytext=(0.02, 0.99), textcoords="axes fraction",
ha="left", va="top", fontsize=8, color="#a63603",
arrowprops=dict(arrowstyle="->", color="#a63603", lw=1.2),
zorder=6)
axes[1].set_title("B. BFS levels from source 0-1-2 "
"(grey 0 / blue 1 / red 2)\nodd level-2 seam "
+ "-".join(map(str, odd_seam)) + " bold red",
fontsize=10)
fig.suptitle("Minimal genuine obstruction (seed2 #26): the programme fails here "
"even after exhausting\nsites x tread-phases x root colour-orders "
"(24 settings, 0 ok) -- a face-leaf / gadget case.", fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.9))
out = os.path.join(HERE, "failing_graph_seed2_26.png")
fig.savefig(out, dpi=160)
print("wrote", out)
@@ -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()
@@ -0,0 +1,166 @@
"""Draw every full medial tire graph from the seed-1 analysis, one note each.
For each M(T) found by tire_realization_analysis.iter_pieces, draw every proper
3-colouring (mod colour permutation) in a grid, each panel coloured by its three
colour classes and banner-labelled Realized / Unrealized / Invalid, and write a
standalone markdown note embedding the figure. Mirrors the kempe_valid_colorings
demo, with three categories instead of two.
"""
from __future__ import annotations
import math
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from tire_realization_analysis import iter_pieces
HERE = os.path.dirname(os.path.abspath(__file__))
CLASS_PALETTE = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # colour classes
CAT_COLOR = {"Realized": "#2ca02c", "Unrealized": "#ff7f0e", "Invalid": "#d62728"}
CAT_ORDER = {"Realized": 0, "Unrealized": 1, "Invalid": 2}
def _positions(g):
n = g.n
matched = g.bite_edges
def ann(k):
a = math.pi / 2 - 2 * math.pi * k / n
return math.cos(a), math.sin(a)
def mid(i):
return math.pi / 2 - 2 * math.pi * (i + 0.5) / n
pos = {f"a{k}": ann(k) for k in range(n)}
for i, t in enumerate(g.tooth_word):
if t == "U":
pos[f"u{i}"] = (1.42 * math.cos(mid(i)), 1.42 * math.sin(mid(i)))
elif i not in matched:
pos[f"d{i}"] = (0.58 * math.cos(mid(i)), 0.58 * math.sin(mid(i)))
for i, j in sorted(g.bites):
corners = [ann(i), ann((i + 1) % n), ann(j), ann((j + 1) % n)]
cx = sum(p[0] for p in corners) / 4.0
cy = sum(p[1] for p in corners) / 4.0
pos[f"p{i}_{j}"] = (cx * 0.82, cy * 0.82)
return pos
def _draw(ax, g, pos, coloring, category):
n = g.n
for u, v in g.edges():
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
color="#cccccc", lw=0.4, zorder=1)
for k in range(n):
a, b = f"a{k}", f"a{(k + 1) % n}"
ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]],
color="#777777", lw=0.8, zorder=2)
for v, (x, y) in pos.items():
big = v.startswith("p")
ax.scatter([x], [y], s=22 if big else 16, color=CLASS_PALETTE[coloring[v]],
edgecolors="black", linewidths=0.3, zorder=3)
ax.set_xlim(-1.6, 1.6)
ax.set_ylim(-1.7, 1.6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(category, fontsize=6, color=CAT_COLOR[category], pad=1.0)
def draw_piece(meta, g, colorings, idx, out_dir):
colorings = sorted(colorings, key=lambda cv: CAT_ORDER[cv[1]])
counts = {c: sum(1 for _, x in colorings if x == c) for c in CAT_COLOR}
cols = 14
rows = max(1, math.ceil(len(colorings) / cols))
fig, axes = plt.subplots(rows, cols, figsize=(cols * 1.15, rows * 1.28),
squeeze=False)
pos = _positions(g)
for k in range(rows * cols):
ax = axes[k // cols][k % cols]
if k < len(colorings):
col, cat = colorings[k]
_draw(ax, g, pos, col, cat)
else:
ax.axis("off")
bites = ",".join(f"({i},{j})" for i, j in sorted(g.bites)) or "none"
fig.suptitle(
f"M(T) from source {meta['source']}, tread T{meta['tread']}: "
f"|A(T)|={g.n}, word={g.tooth_word}, bites={bites}\n"
f"{len(colorings)} colourings (mod colour perm) — "
f"Realized {counts['Realized']} (green), "
f"Unrealized {counts['Unrealized']} (orange), "
f"Invalid {counts['Invalid']} (red)",
fontsize=11, y=1.0 - 0.0,
)
fig.tight_layout(rect=(0, 0, 1, 0.985))
base = f"piece_{idx:02d}_src{meta['source']}_T{meta['tread']}"
png = os.path.join(out_dir, base + ".png")
pdf = os.path.join(out_dir, base + ".pdf")
fig.savefig(png, dpi=110)
fig.savefig(pdf)
plt.close(fig)
note = os.path.join(out_dir, base + ".md")
with open(note, "w") as fh:
fh.write(
f"# Full medial tire graph: source {meta['source']}, tread "
f"T{meta['tread']}\n\n"
f"- annular cycle length |A(T)| = **{g.n}**\n"
f"- tooth word: `{g.tooth_word}` "
f"({len(g.up_edges)} up, {len(g.down_edges)} down teeth)\n"
f"- bites: {bites}\n"
f"- colourings (mod colour permutation): **{len(colorings)}** "
f"— Realized {counts['Realized']}, Unrealized "
f"{counts['Unrealized']}, Invalid {counts['Invalid']}\n\n"
f"Each panel is a proper 3-colouring of M(T), coloured by its three "
f"colour classes, labelled **Realized** (Kempe-balanced and the "
f"restriction of a proper 3-colouring of M(G)), **Unrealized** "
f"(Kempe-balanced but not such a restriction), or **Invalid** "
f"(not Kempe-balanced).\n\n"
f"![colourings]({base}.png)\n\n"
f"Vector copy: [`{base}.pdf`]({base}.pdf).\n"
)
return base, counts
def main(seed: int = 1):
out_dir = os.path.join(HERE, f"tire_realization_seed{seed}")
os.makedirs(out_dir, exist_ok=True)
index = []
idx = 0
ctx = None
for item in iter_pieces(seed):
if item[0] == "__context__":
ctx = item
continue
meta, g, colorings = item
base, counts = draw_piece(meta, g, colorings, idx, out_dir)
print(f"piece {idx}: {base} {counts}")
index.append((idx, meta, g, counts, base))
idx += 1
_, G, M, n_global = ctx
with open(os.path.join(out_dir, "README.md"), "w") as fh:
fh.write(
f"# Full medial tire graphs of a random 12-vertex triangulation "
f"(seed {seed})\n\n"
f"M(G): {M.number_of_nodes()} medial vertices, {n_global} proper "
f"3-colourings. {len(index)} full medial tire graphs, one note each "
f"below.\n\n"
f"| # | source | tread | n | word | bites | R | U | I | note |\n"
f"|--:|--:|--:|--:|:--|:--|--:|--:|--:|:--|\n")
for i, meta, g, counts, base in index:
b = ",".join(f"({x},{y})" for x, y in sorted(g.bites)) or "-"
fh.write(
f"| {i} | {meta['source']} | T{meta['tread']} | {g.n} | "
f"`{g.tooth_word}` | {b} | {counts['Realized']} | "
f"{counts['Unrealized']} | {counts['Invalid']} | "
f"[{base}.md]({base}.md) |\n")
print(f"wrote {len(index)} notes to {out_dir}")
if __name__ == "__main__":
main()
@@ -0,0 +1,238 @@
"""Step-by-step picture of the even-level-cycle programme on the smallest clean
example: the ring triangulation sizes=[3,5], leaf='hub' (rng seed 0), 9 vertices.
One odd level cycle (level 1, the 5-cycle 3-4-5-6-7), no terminal triangles, so
the only surgery is a single DIAMOND. We walk the FIRST successful choice-set
found by the sweep: insertion site = edge (3,4); colour phase = (0,); root DFS
colour order = (1,0,2). Panels:
A G with its odd level-5 seam (BFS levels from the outer triangle 0-1-2).
B G' = G + diamond w(=9) on edge (3,4): seam is now an even 6-cycle; the
diamond quad 3-0-4-8 (restored diagonal 3-4) shaded.
C medial M(G') with the canonical colouring BEFORE any switch: the four quad
medials m(0,3),m(0,4),m(4,8),m(3,8) are ALL colour 1 -> diamond_condition
fails (the obstruction).
D after one {1,2}-Kempe switch on the component through m(0,3)
{(0,3),(3,7),(3,8),(3,9)}: quad medials become 2,1,1,2 -> reducible;
remove w, restored diagonal (3,4) takes the third colour 0.
Embedding: networkx planar_layout (canonical-ordering straight-line embedding),
recentred, with EVERY panel verified crossing-free before drawing -- G, G', and
the medial M(G') drawn at edge midpoints. G' is embedded once and reused for
panels B/C/D; G reuses it (minus w, with the diagonal 3-4 restored) when that is
still crossing-free, else it is embedded independently. Run with the repo venv
python (numpy + matplotlib + networkx).
"""
import os, random
import numpy as np
import networkx as nx
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import kempe_even_program_harness as H
HERE = os.path.dirname(os.path.abspath(__file__))
PAL = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # colours "1","2","3"(=2)
def planar_pos(g):
nxg = nx.Graph()
nxg.add_nodes_from(g.rot)
for ed in g.edges():
a, b = tuple(ed); nxg.add_edge(a, b)
ok, _ = nx.check_planarity(nxg)
assert ok, "graph not planar?!"
pos = nx.planar_layout(nxg)
pts = np.array([pos[v] for v in g.rot]); c = pts.mean(axis=0)
s = np.abs(pts - c).max()
return {v: ((pos[v][0]-c[0])/s, (pos[v][1]-c[1])/s) for v in g.rot}
def seg_cross(p, q, r, s):
def o(a, b, c):
return (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0])
d1, d2, d3, d4 = o(r, s, p), o(r, s, q), o(p, q, r), o(p, q, s)
return ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0))
def crossings(edges, pos):
bad = []
for i in range(len(edges)):
a, b = edges[i]
for j in range(i+1, len(edges)):
c, d = edges[j]
if len({a, b, c, d}) < 4:
continue
if seg_cross(pos[a], pos[b], pos[c], pos[d]):
bad.append((edges[i], edges[j]))
return bad
def mid(p, q): return ((p[0]+q[0])/2, (p[1]+q[1])/2)
# ---- build G and G' ------------------------------------------------------
rng = random.Random(0)
g, outer = H.ring_triangulation([3, 5], 'hub', rng)
an = H.Analysis(g.copy(), outer)
ring = [c for k, c in an.seams if k == 1][0]
prep = H._prep_gadgets(g.copy(), outer)
template, an_g, gadgets = prep
gg = template.copy()
w, u, v, x, t = gg.insert_diamond(3, 4)
an2 = H.Analysis(gg, outer)
ring2 = [c for k, c in an2.seams if k == 1][0]
quad = H.quad_of(gg, w, u, v) # (3,0,4,8)
# embed G' (verified), reuse for G if still crossing-free else embed G alone
posGp = planar_pos(gg)
edgesGp = [tuple(e) for e in gg.edges()]
badGp = crossings(edgesGp, posGp)
print("G' crossings:", badGp if badGp else "NONE")
assert not badGp
posG = {vv: posGp[vv] for vv in g.rot}
edgesG = [tuple(e) for e in g.edges()]
badG = crossings(edgesG, posG)
if badG:
print("G reuse crossed; embedding G independently")
posG = planar_pos(g)
badG = crossings([tuple(e) for e in g.edges()], posG)
print("G crossings:", badG if badG else "NONE")
assert not badG
# medial drawn at edge midpoints. The medial drawing at midpoints is planar
# EXCEPT for the medial triangle of whichever face is geometrically OUTER
# (its three midpoint-chords would cut straight across the unbounded region), so
# we omit exactly those three edges, then verify the rest are crossing-free.
def convex_hull(points):
pts = sorted(points)
def cross(o, a, b):
return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])
lo = []
for p in pts:
while len(lo) >= 2 and cross(lo[-2], lo[-1], p) <= 0:
lo.pop()
lo.append(p)
up = []
for p in reversed(pts):
while len(up) >= 2 and cross(up[-2], up[-1], p) <= 0:
up.pop()
up.append(p)
return lo[:-1] + up[:-1]
adjm = H.medial_adj(gg)
mpos = {m: mid(posGp[tuple(m)[0]], posGp[tuple(m)[1]]) for m in adjm}
hull = convex_hull(list(posGp.values()))
pos2v = {tuple(p): v for v, p in posGp.items()}
outer_face = {pos2v[tuple(p)] for p in hull}
print("geometric outer face (hull):", sorted(outer_face))
medges = []
seen = set()
for m in adjm:
for b in adjm[m]:
k = frozenset((m, b))
if k in seen:
continue
seen.add(k)
# skip the medial edge joining two edges of the outer face
if set(m) <= outer_face and set(b) <= outer_face:
continue
medges.append((m, b))
badM = crossings(medges, mpos)
print("M(G') crossings (outer-face medial triangle omitted):",
badM if badM else "NONE")
assert not badM
# ---- colourings ----------------------------------------------------------
col0, _ = H.canonical_coloring_explicit(gg, an2.level, outer, (0,), [1, 0, 2])
col1 = dict(col0)
comp = H.kempe_component(col1, adjm, H.e(0, 3), (1, 2))
H.switch(col1, comp, (1, 2))
third = H.diamond_condition(col1, quad)
col1[H.e(3, 4)] = third
# ---- drawing -------------------------------------------------------------
def lims(ax, pos):
xs = [p[0] for p in pos.values()]; ys = [p[1] for p in pos.values()]
ax.set_xlim(min(xs)-0.25, max(xs)+0.25); ax.set_ylim(min(ys)-0.25, max(ys)+0.3)
def draw_graph(ax, gr, pos, level=None, bold_cycle=None, shade_quad=None, wvert=None):
if shade_quad:
ax.add_patch(Polygon([pos[vv] for vv in shade_quad], closed=True,
color="#ffe2bf", zorder=0))
bold = set()
if bold_cycle:
for i in range(len(bold_cycle)):
bold.add(frozenset((bold_cycle[i], bold_cycle[(i+1) % len(bold_cycle)])))
for ed in gr.edges():
a, b = tuple(ed); pa, pb = pos[a], pos[b]
hot = ed in bold
ax.plot([pa[0], pb[0]], [pa[1], pb[1]],
color="#d62728" if hot else "#888888",
lw=2.8 if hot else 1.1, zorder=2)
for vv, p in pos.items():
c = "#d62728" if (wvert is not None and vv == wvert) else "#222222"
ax.plot(*p, "o", ms=18, mfc="white", mec=c, mew=1.7, zorder=5)
ax.annotate(str(vv), p, ha="center", va="center", fontsize=9,
fontweight="bold", color=c, zorder=6)
if level is not None:
ax.annotate(f"L{level[vv]}", p, textcoords="offset points",
xytext=(11, 10), fontsize=6.5, color="#999999", zorder=6)
def draw_medial(ax, pos, col, halo=None, restored=None):
# medial graph only -- no base graph underneath
for m, b in medges:
pa, pb = mpos[m], mpos[b]
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#c6c6c6", lw=0.8, zorder=1)
if restored is not None:
a, b = restored
ax.plot(*mid(pos[a], pos[b]), "s", color=PAL[col[H.e(a, b)]], ms=13,
mec="#d62728", mew=2.0, zorder=7)
halo = halo or set()
for m, p in mpos.items():
if m not in col:
continue
if m in halo:
ax.plot(*p, "o", color="#000000", ms=16, zorder=5)
ax.plot(*p, "o", color=PAL[col[m]], ms=10.5, mec="black", mew=0.8, zorder=6)
fig, axes = plt.subplots(1, 4, figsize=(19, 5.4))
for ax in axes:
ax.set_aspect("equal"); ax.axis("off")
draw_graph(axes[0], g, posG, level=an.level, bold_cycle=ring)
lims(axes[0], posG)
axes[0].set_title("A. G (BFS levels from source triangle 0-1-2)\n"
"odd level-1 seam = 5-cycle 3-4-5-6-7 (red)\n"
"verified straight-line planar embedding", fontsize=9)
draw_graph(axes[1], gg, posGp, level=an2.level, bold_cycle=ring2,
shade_quad=quad, wvert=w)
lims(axes[1], posGp)
axes[1].set_title("B. G' = G + diamond w=9 on edge (3,4)\n"
"seam now even 6-cycle; quad 3-0-4-8 shaded", fontsize=9)
quad_med = {H.e(quad[i], quad[(i+1) % 4]) for i in range(4)}
draw_medial(axes[2], posGp, col0, halo=quad_med)
lims(axes[2], posGp)
axes[2].set_title("C. M(G') canonical colour (phase 0, DFS order 1,0,2)\n"
"quad medials m(0,3)m(0,4)m(4,8)m(3,8) ALL =1 (haloed)\n"
"-> diamond_condition FAILS", fontsize=9)
draw_medial(axes[3], posGp, col1, halo=comp, restored=(3, 4))
lims(axes[3], posGp)
axes[3].set_title("D. after {1,2}-Kempe switch on comp through m(0,3)\n"
"{(0,3),(3,7),(3,8),(3,9)} (haloed): quad -> 2,1,1,2\n"
f"remove w; restored edge (3,4)=square takes colour {third}",
fontsize=9)
fig.suptitle("Even-level-cycle programme, worked example (ring [3,5]+hub, 9 "
"vertices): one odd seam -> one diamond -> one Kempe switch -> "
"proper 3-colouring of M(G). Colours: 1=orange(0), 2=blue(1), "
"3=green(2).", fontsize=10)
fig.tight_layout(rect=(0, 0, 1, 0.9))
out = os.path.join(HERE, "even_program_walkthrough.png")
fig.savefig(out, dpi=160)
print("wrote", out)
@@ -0,0 +1,90 @@
"""Print every stage of the even-level-cycle programme on the smallest clean
example (ring sizes=[3,5], leaf='hub', rng seed 0; 9 vertices) for the first
choice-set the sweep succeeds on: site (3,4), phase (0,), DFS order (1,0,2).
This is the textual companion to even_program_walkthrough.md / .png.
"""
import random
import kempe_even_program_harness as H
def fz(m): # pretty-print an edge-medial
return tuple(sorted(tuple(m)))
def main():
rng = random.Random(0)
g, outer = H.ring_triangulation([3, 5], 'hub', rng)
print("OUTER (source/root triangle):", outer)
print("\n# STEP 1: graph G (rotation system: vertex -> embedding-order neighbours)")
for v in sorted(g.rot):
print(f" {v}: {g.rot[v]}")
print("faces:", [tuple(f) for f in g.faces()])
print("\n# STEP 2: levels (BFS from the source triangle) + seams")
an = H.Analysis(g.copy(), outer)
for v in sorted(an.level):
print(f" v{v}: level {an.level[v]}")
for k, cyc in an.seams:
print(f" seam level {k}: {cyc} (len {len(cyc)}, "
f"{'ODD' if len(cyc) % 2 else 'even'})")
print(" terminal triangles (need leaf gadget):", an.terminal)
print("\n# STEP 3: diamond sites + chosen edge")
template, an_g, gadgets = H._prep_gadgets(g.copy(), outer)
sites = H._candidate_sites(an_g)
print(" gadgets inserted:", gadgets)
print(" candidate diamond edges (odd seam):", sites)
combo = ((3, 4),)
print(" chosen combo (first successful):", combo)
gg = template.copy()
dia = [gg.insert_diamond(a, b) for (a, b) in combo]
print(" inserted (w,u,v,x,t):", dia)
an2 = H.Analysis(gg, outer)
print(" rot[w]:", gg.rot[dia[0][0]], " level[w]:", an2.level[dia[0][0]])
for k, cyc in an2.seams:
print(f" seam level {k} now: len {len(cyc)} "
f"{'ODD' if len(cyc) % 2 else 'even'} {cyc}")
print("\n# STEP 4: medial graph M(G') (one vertex per edge of G')")
adj = H.medial_adj(gg)
print(f" |V(M)| = {len(adj)}")
for m in sorted(adj, key=fz):
print(f" m{fz(m)}: {sorted(fz(b) for b in adj[m])}")
print("\n# STEP 5: canonical colouring phases=(0,) colorder=(1,0,2)")
phases, colorder = (0,), [1, 0, 2]
sk, _ = H.coloring_skeleton(gg, an2.level, outer)
for i, cyc in enumerate(sk['nonroot']):
print(f" non-root annulus #{i} (len {len(cyc)}): {[fz(m) for m in cyc]}")
print(" root annulus:", [fz(m) for m in sk['root']])
print(" outer-trio (free/DFS):", [fz(m) for m in sk['outer_es']])
col, _ = H.canonical_coloring_explicit(gg, an2.level, outer, phases, colorder)
for m in sorted(col, key=fz):
print(f" m{fz(m)} = {col[m]}")
print("\n# STEP 6: Kempe switch + diamond collapse")
w, u, v, x, t = dia[0]
quad = H.quad_of(gg, w, u, v)
support = [H.e(quad[i], quad[(i + 1) % 4]) for i in range(4)]
print(f" diamond w={w}, quad {quad} (diagonal {u}-{v})")
print(" quad medials:", [fz(s) for s in support])
print(" diamond_condition BEFORE switch:", H.diamond_condition(col, quad),
" support:", {fz(s): col[s] for s in support})
adjm = H.medial_adj(gg)
comp = H.kempe_component(col, adjm, H.e(0, 3), (1, 2))
print(" switch {1,2}-component through m(0,3):", sorted(fz(m) for m in comp))
H.switch(col, comp, (1, 2))
third = H.diamond_condition(col, quad)
print(" diamond_condition AFTER switch:", third,
" support:", {fz(s): col[s] for s in support})
H.collapse_degree4(gg, col, w, u, v)
col[H.e(u, v)] = third
print(f" removed w; restored edge ({u},{v}) takes colour {third}")
print(" proper 3-colouring of M(G)?", H.verify_proper(gg, col))
print(" vertices back to original G?", set(gg.rot) == set(g.rot))
if __name__ == "__main__":
main()
@@ -0,0 +1,141 @@
# The even-level-cycle colouring program
A constructive route distinct from the `R_T` composition line. Idea: surger a
triangulation `G` so that **every level cycle is even**, take the resulting
*canonical even colouring* of `M(G')` (no 4CT used), then **remove the planted
vertices** by Kempe switches, landing on a proper 3-colouring of `M(G)` — i.e. a
Tait/4CT colouring of `G`.
Scripts: `kempe_even_program_harness.py`, `draw_evened_leaf.py` (`evened_leaf.png`).
## The two surgeries
- **Leaf gadget (two vertices).** On a terminal triangle `uvt` with outer apex
`x`: add `y = mid(uv)` and hub `z`; delete `uv`; add `xy, uy, vy, zy, zu, zv,
zt`. Both new vertices have degree 4; the seam becomes `u-y-v-t` (even) and the
leaf becomes a **4-wheel** with hub `z`. No ears, no chord — the monochromatic-3
seam stays valid, so **leaves create no colouring defect**. (Earlier one-vertex
chord version forced a `{0011,0101}` defect; this is strictly better.)
- **Diamond.** On an odd internal seam edge `uv` with apexes `x,t`: delete `uv`,
add `w ~ u,v,x,t` (degree 4). Flips that seam's parity.
By `n_T = p + Σq_i + 2b`, evening every internal seam makes every annular cycle
even **except the root** (the outer triangle's odd charge `Σ_T n_T ≡ 3 (mod 2)` is
invariant — confirmed; the root is handled as the one unavoidable defect region,
solved by local backtracking).
## Canonical even colouring (constructive, no 4CT)
Every level-edge medial vertex → colour 3; every non-root annular cycle alternates
1,2; the root region solved by DFS. Proper because each apex is forced 3 between
two `{1,2}` pairs and (in the non-degenerate tread model) no two level edges are
consecutive around a vertex or face.
## Removal conditions (degree-4 Kempe reduction — the historically *safe* case)
- **Diamond** `w` (quad `u-x-v-t`, restore diagonal `uv`): removable iff the pair
`(m_ux,m_xv)` is distinct, `(m_vt,m_tu)` is distinct, ≤2 colours total; then
`m_uv` takes the third.
- **Gadget**: collapse `z` then `y` (or `y` then `z`), ending in a degree-3
unstellation needing a rainbow triangle. Two orders = free choice.
## Status (synthetic ring triangulations, the clean-level-structure domain)
Pipeline runs end to end. Surgery, canonical colouring, and gadget removal all
work. The program now lands squarely on the **cycle layer**.
The original `60 random ring triangulations: 39 ok, 21 fail` figure was the
**first-match heuristic** — one diamond per odd seam, placed at the *first*
admissible seam edge, only the colouring phase varied (≤4 random tread phases).
That is one point in the insertion-site design space, not a sweep of it.
**Site sweep (`run_graph` now enumerates every combination of insertion sites,
one per odd seam, ≤4 colour phases each; `--max-combos` caps the product).**
A graph counts `ok` iff *some* placement fully descends:
```
seed 1, 60 graphs: first-match 31 ok / 29 fail -> sweep 54 ok / 6 fail (rescued 23)
seed 2, 60 graphs: first-match 36 ok / 24 fail -> sweep 57 ok / 3 fail (rescued 21)
```
(First-match is seed-sensitive — 3139 depending on seed; the 39 was one such
seed. What is robust is the *gap*: sweeping insertion sites rescues ~20 of the
~24 first-match failures, leaving a small stubborn residue of ~36
`fail:diamond-switch` graphs.) Design space is real but modest: ~50 graphs need
a diamond, ~2900 combos total, max ~9001200 on a single graph (a handful hit
the cap). So the answer to "did we test every way of adding a diamond?" is:
**now yes** (per odd seam, up to the cap), and most of the apparent failures
were heuristic, not intrinsic.
**Crucial diagnostic:** for a failing case, a simultaneously-removable proper
3-colouring of `M(G')` was shown to **exist** (it must — `M(G)` is 3-colourable).
So `fail:diamond-switch` is **not** non-existence; it is **Kempe-reachability**
whether switches carry the *canonical even* colouring to a descendable one. That is
exactly the conjecture's core, and the harness has localised the entire program
difficulty to it, with everything upstream constructive.
**Why greedy fails (and what's next).** Diamonds on different odd seams share
*vertical* `{1,3}`-Kempe cycles, so per-diamond local switching cannot satisfy them
simultaneously. The principled solve is joint: vertices = `{1,3}`-Kempe cycles,
one edge per diamond joining its two side cycles; removability for all diamonds at
once = a consistent XOR assignment = **bipartiteness** of that graph (no self-loop =
the side cycles differ; no odd cycle = no three diamonds whose side cycles form a
triangle). Insertion-site choice (which seam edge) and tread phase are the control
knobs. Building this joint solver — and finding the smallest configuration, if any,
forcing a self-loop or odd cycle — is the next step and the exact thing a proof
would need to rule out.
## Exhausting the control knobs over the residue
The site sweep above counts a graph `ok` if some placement works over only **4
random** colour phases per combo. `residue_phase_sweep.py` takes the graphs that
sweep still fails (the residue) and **exhaustively enumerates the colour/tread
phase and the root-DFS colour order** on top of every insertion site:
```
phases in {0,1}^A (A = # non-root annuli; the tread phases)
colorder in perms(0,1,2) (root-region DFS colour priority)
```
over all site combos (cap 512). Result on the seed-1/seed-2 residue
(`residue_phase_sweep_results.txt`):
```
seed1 #16 [3,8,3,5] hub RESCUED (720 settings, 18 ok)
seed1 #51 [3,7,3,3,7] hub RESCUED (42336 settings, 196 ok)
seed1 #3 [3,7,4,6,3] face STILL FAILS (672 settings, 0 ok)
seed1 #4 [3,4,5,5,3] face STILL FAILS (2400 settings, 0 ok)
seed2 #26 [3,6,3] face STILL FAILS (24 settings, 0 ok)
seed2 #30 [3,3,6,7,3] face STILL FAILS (2016 settings, 0 ok)
seed2 #54 [3,3,5,3] face STILL FAILS (720 settings, 0 ok)
```
Two things fall out:
1. **Phase reachability explains part of the residue.** The two `hub` graphs are
*rescued* once the phase/colour-order is enumerated rather than sampled — they
were never genuine obstructions, just unlucky random phases. So the
random-phase `fail` count overstates the true difficulty.
2. **The genuine obstructions are exactly the `face`-leaf graphs.** Every graph
that survives exhausting sites × phases × colour-orders has `leaf='face'`
i.e. an inner terminal triangle carrying a leaf gadget. The smallest is
`seed2 #26 [3,6,3]` (one site combo, 24 settings, all fail at
`gadget-removal`): a minimal target for the joint solver / an obstruction
hunt. (#26 fails at the gadget step; #3/#4/#30/#54 at `diamond-switch`.)
**Caveat on "STILL FAILS".** `try_establish` is a *bounded* local Kempe search
(≤3 components anchored at the quad support). So `STILL FAILS` means *no (site,
phase, colour-order) lets the bounded search from the canonical-even colouring
reach a descendable one* — not that no Kempe path exists. A descendable colouring
provably exists (M(G) is 3-colourable); whether it is reachable under a principled
(joint, unbounded) switch is the open question, now sharply localised to the
`face`-leaf family.
## Caveats / domain
- Real plantri triangulations mostly `skip:chord-level-edge` under BFS-from-outer
level structure — a reflection of how restrictive the clean nested-tire level
structure is, not a harness bug. The synthetic concentric-ring generator produces
the clean domain the program is stated for.
- Root defect and the (deferred) outer-face handling are localised; the user has a
separate idea for the outer face.
@@ -0,0 +1,196 @@
# Even-level-cycle programme — a fully worked example
A step-by-step trace of the whole pipeline on the **smallest clean graph**: the
synthetic ring triangulation `sizes=[3,5]`, `leaf='hub'` (generator
`random.Random(0)`), **9 vertices**. It has exactly one odd level cycle and no
terminal triangles, so the only surgery is a **single diamond** — which makes
every stage small enough to print in full.
Everything below is the *actual* state produced by `kempe_even_program_harness.py`
(regenerate the data with the dump at the end of this note; the figure is
`even_program_walkthrough.png`, drawn by `draw_walkthrough.py`). We walk the
**first choice-set the sweep finds that succeeds**:
> insertion site = edge `(3,4)` · colour phase = `(0,)` · root-DFS colour order = `(1,0,2)`
Colour convention throughout: values `{0,1,2}` are Tait colours "1,2,3"; `2` is
the "colour 3" the seam is painted with. In the figure: `0`=orange, `1`=blue,
`2`=green.
---
## Step 1 — Generate the triangulation with a plane embedding *(panel A)*
`ring_triangulation([3,5], 'hub')` builds three concentric rings — an outer
triangle, a 5-ring, and a hub — triangulating each annulus with a random tooth
word and capping the centre with a hub vertex. The result is a genuine plane
triangulation given by its rotation system (neighbours in embedding order):
```
0: [4, 3, 7, 6, 5, 2, 1] 3: [0, 4, 8, 7] 6: [5, 0, 7, 8]
1: [4, 0, 2, 5] 4: [3, 0, 1, 5, 8] 7: [6, 0, 3, 8]
2: [5, 1, 0] 5: [4, 1, 2, 0, 6, 8] 8: [3, 4, 5, 6, 7]
```
The 14 triangular faces (one is the outer/unbounded face) are
```
(4,3,8) (4,0,3) (4,1,0) (4,5,1) (4,8,5) (3,0,7) (3,7,8)
(0,6,7) (0,5,6) (0,2,5) (0,1,2) (1,5,2) (5,8,6) (6,8,7)
```
and the 21 edges are the pairs appearing above. (Euler check: 9 21 + 14 = 2.)
The figure draws G (and G, and the medial M(G) at edge midpoints) with a
straight-line planar embedding from `networkx.planar_layout`, each **verified
crossing-free** before rendering (the medial triangle of the geometric outer
face is omitted, since its midpoint-chords would otherwise cut across the
unbounded region).
## Step 2 — Pick the source and read off levels *(panel A)*
The **source** is the outer triangle, taken as the unbounded face `(0,1,2)`.
A BFS from those three vertices assigns each vertex its **level** (graph distance
to the source tread):
| level | vertices |
|------:|----------|
| 0 | 0, 1, 2 (the source triangle) |
| 1 | 3, 4, 5, 6, 7 (the ring) |
| 2 | 8 (the hub) |
The **level cycles ("seams")** are the same-level edge cycles at each depth ≥1:
```
level 1: cycle 3-4-5-6-7 length 5 -> ODD
```
There is exactly one seam and it is **odd**. There are **no terminal
triangles**, so the leaf gadget never fires — the only surgery needed is a
diamond on this one odd seam.
## Step 3 — Choose the edge(s) that make the level cycles even *(panel B)*
A diamond can be inserted on any seam edge whose two apexes straddle the
neighbouring levels (`k1` and `k+1`). For the level-1 seam, **all five** seam
edges qualify:
```
candidate diamond sites: (3,4) (4,5) (5,6) (6,7) (7,3)
```
This is the choice the **site sweep** ranges over (here a 5-element design
space). We take the **first one that leads to a full success: `(3,4)`**.
Insert the diamond on `(3,4)`:
- delete the edge `(3,4)`;
- add a new degree-4 vertex `w = 9` adjacent to `u=3, v=4` and the two apexes
`x=0` (level 0) and `t=8` (level 2), with rotation `rot[9] = [3,0,4,8]`.
`w` lands at level 1, so the level-1 seam becomes the cycle
```
3-9-4-5-6-7 length 6 -> EVEN
```
Every level cycle is now even. The four-cycle `3-0-4-8` around `w` (diagonal the
restored edge `3-4`) is the **diamond quad** we must later collapse — shaded in
panel B.
## Step 4 — Build the medial graph M(G) *(panels C, D)*
The medial graph has **one vertex per edge of G** (24 of them) and joins two
edge-medials iff the edges are consecutive around a common face. A 4-colouring
of the triangulation = a proper **3-colouring of M(G)**. The adjacency (each
medial `m(a,b)` listed with its neighbours):
```
m(0,1): (0,2)(0,4)(1,2)(1,4) m(3,7): (0,3)(0,7)(3,8)(7,8)
m(0,2): (0,1)(0,5)(1,2)(2,5) m(3,8): (3,7)(3,9)(7,8)(8,9)
m(0,3): (0,7)(0,9)(3,7)(3,9) m(3,9): (0,3)(0,9)(3,8)(8,9)
m(0,4): (0,1)(0,9)(1,4)(4,9) m(4,5): (1,4)(1,5)(4,8)(5,8)
m(0,5): (0,2)(0,6)(2,5)(5,6) m(4,8): (4,5)(4,9)(5,8)(8,9)
m(0,6): (0,5)(0,7)(5,6)(6,7) m(4,9): (0,4)(0,9)(4,8)(8,9)
m(0,7): (0,3)(0,6)(3,7)(6,7) m(5,6): (0,5)(0,6)(5,8)(6,8)
m(0,9): (0,3)(0,4)(3,9)(4,9) m(5,8): (4,5)(4,8)(5,6)(6,8)
m(1,2): (0,1)(0,2)(1,5)(2,5) m(6,7): (0,6)(0,7)(6,8)(7,8)
m(1,4): (0,1)(0,4)(1,5)(4,5) m(6,8): (5,6)(5,8)(6,7)(7,8)
m(1,5): (1,2)(1,4)(2,5)(4,5) m(7,8): (3,7)(3,8)(6,7)(6,8)
m(2,5): (0,2)(0,5)(1,2)(1,5) m(8,9): (3,8)(3,9)(4,8)(4,9)
```
## Step 5 — Canonical colouring (no 4CT): seam = 3, annuli alternate, root by DFS *(panel C)*
The canonical colouring is assembled from three deterministic ingredients plus
the two control knobs (phase, DFS order):
1. **Every level-edge medial → colour 3 (=2).** The even seam `3-9-4-5-6-7`
becomes **monochromatic 3**:
`m(3,9)=m(4,9)=m(4,5)=m(5,6)=m(6,7)=m(3,7)=2`.
2. **Each non-root annulus alternates {0,1} with a phase bit.** Here there is one
non-root annulus — the hub spokes between levels 1 and 2:
`[(8,9),(3,8),(7,8),(6,8),(5,8),(4,8)]` (length 6). With **phase 0** it is
coloured `0,1,0,1,0,1`:
`m(8,9)=0, m(3,8)=1, m(7,8)=0, m(6,8)=1, m(5,8)=0, m(4,8)=1`.
3. **The root region** — the level-0↔1 spokes plus the three outer-triangle
medials `m(0,1),m(0,2),m(1,2)` — is solved by a small DFS using colour
priority **`(1,0,2)`**.
The resulting proper colouring of M(G):
```
m(0,1)=2 m(0,2)=1 m(0,3)=1 m(0,4)=1 m(0,5)=0 m(0,6)=1 m(0,7)=0 m(0,9)=0
m(1,2)=0 m(1,4)=0 m(1,5)=1 m(2,5)=2 m(3,7)=2 m(3,8)=1 m(3,9)=2 m(4,5)=2
m(4,8)=1 m(4,9)=2 m(5,6)=2 m(5,8)=0 m(6,7)=2 m(6,8)=1 m(7,8)=0 m(8,9)=0
```
This is the "no-4CT" colouring of the **evened** graph — proper because the seam
is even (a monochromatic-3 cycle around even-length annuli is consistent). The
only thing standing between it and a colouring of the *original* G is the
diamond.
## Step 6 — Kempe switch, then collapse the diamond *(panel D)*
To remove `w=9` we restore the diagonal `(3,4)` and recolour. The **degree-4
removal condition** on the quad `3-0-4-8` reads: the opposite-corner medial pairs
`(m_{30}, m_{04})` and `(m_{48}, m_{83})` must each be *distinct*, using ≤2
colours total; the restored edge then takes the third.
Read the four quad medials off the canonical colouring:
```
m(0,3)=1 m(0,4)=1 m(4,8)=1 m(3,8)=1 ALL EQUAL
```
The first pair `(m_{30},m_{04}) = (1,1)` is **not** distinct → `diamond_condition`
returns `None`. **This is the obstruction** the bare canonical colouring hits
(haloed in panel C). It is *not* non-existence — a removable colouring exists; we
just have to reach one by a Kempe switch.
**The switch.** The bounded search picks the **`{1,2}`-Kempe component through
`m(0,3)`**:
```
component = { m(0,3), m(3,7), m(3,8), m(3,9) } (pair {1,2})
```
Swapping colours `1↔2` on this component flips `m(0,3): 1→2` and `m(3,8): 1→2`
(the other two are already in `{1,2}` and toggle within the class). The quad
medials become:
```
m(0,3)=2 m(0,4)=1 m(4,8)=1 m(3,8)=2
```
Now `(m_{30},m_{04}) = (2,1)` distinct ✓ and `(m_{48},m_{83}) = (1,2)` distinct ✓,
two colours `{1,2}` used → `diamond_condition` returns the **third colour 0**.
**Collapse.** Delete `w`, restore edge `(3,4)`, and colour the restored medial
`m(3,4) = 0` (the orange square in panel D). The result is verified to be a
**proper 3-colouring of M(G)** on exactly the original 9 vertices — i.e. a
Tait/4CT colouring of the original triangulation, obtained with no appeal to the
4CT.
---
## Reproduce
```bash
python3 dump_walkthrough.py # prints every step's data verbatim
../../../.venv/bin/python draw_walkthrough.py # the 4-panel figure (repo venv: numpy+matplotlib)
```
The reduction here genuinely exercises a Kempe switch. For larger graphs the
same six steps run with more diamonds (one per odd seam, swept over all sites)
and more phase/colour-order choices; the open difficulty is purely whether some
(site, phase) choice lets every diamond's quad become reducible simultaneously
— see `even_program_findings.md`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

@@ -0,0 +1,17 @@
"""Compatibility wrapper for the medial tire generator now kept in ../lib."""
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.full_medial_tire_generator import * # noqa: F401,F403
from lib.full_medial_tire_generator import main
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

@@ -0,0 +1,69 @@
# Atlas of full medial tire graphs with |A(T)| = 9
This note collects every full medial tire graph whose annular cycle `A(T)` has
nine vertices, generated exhaustively from the structural properties in
Definitions/Remarks 3.13.9 of `paper.tex`.
## What is being enumerated
A full medial tire graph of size `n = |A(T)|` is determined by:
- a tooth word in `{U, D}^n` — one up (`U`) or down (`D`) tooth per annular
edge (Def. 3.4), with **at least three up teeth** (Rem. 3.6);
- a **non-crossing matching** of the down edges into *bites* — pairs of down
teeth sharing an apex (Rem. 3.5, Def. 3.7); unmatched down teeth are
singletons. The two annular edges of a bite must be **non-incident**
(Def. 3.7): they share no annular vertex, so cyclically adjacent edges
cannot pair;
- subject to the **bite-face condition** (Rem. 3.8): in `B(T) = A(T) + bite
apexes`, every interior non-tooth face must contain `0` or `≥ 3`
down-tooth apexes in its interior (equivalently, no face holds exactly one
or two singleton down teeth).
Graphs are identified up to the dihedral symmetry of the annular cycle
(rotations and reflections), since these give isomorphic plane graphs.
## The atlas
![All full medial tire graphs with |A(T)| = 9](full_medial_tire_n9.png)
High-resolution vector copy: [`full_medial_tire_n9.pdf`](full_medial_tire_n9.pdf).
Full textual index: [`full_medial_tire_n9_index.txt`](full_medial_tire_n9_index.txt).
In each diagram the thick black ring is `A(T)`; **blue** outer apexes are up
teeth, **red** inner apexes are singleton down teeth, and a **dark-red** inner
apex with four spokes is a bite (its two paired annular edges). The label under
each diagram is the tooth word and the bite pairs (edge indices).
## Counts
There are **81** classes for `n = 9` (cf. `3:1, 4:1, 5:2, 6:6, 7:13, 8:36,
9:81` for `n = 3..9`, with the non-incidence stipulation in force). Breakdown
of the 81 classes:
| down teeth | classes | | bites | classes | | up teeth | classes |
|-----------:|--------:|---|------:|--------:|---|---------:|--------:|
| 0 | 1 | | 0 | 35 | | 3 | 23 |
| 2 | 3 | | 1 | 35 | | 4 | 29 |
| 3 | 7 | | 2 | 8 | | 5 | 18 |
| 4 | 18 | | 3 | 3 | | 6 | 7 |
| 5 | 29 | | | | | 7 | 3 |
| 6 | 23 | | | | | 9 | 1 |
46 of the 81 classes contain at least one bite. (Every singleton down tooth
must sit in a face holding `≥ 3` of them, so e.g. words with exactly one or two
down teeth only survive when those down teeth are paired into a bite — and now
only when the paired edges are non-incident, which is why the counts fall
sharply from the unrestricted `n = 9` total of 159.)
## Reproduce
```sh
# from this directory, using the repo .venv
../../../.venv/bin/python plot_full_medial_tire_n9.py # figure + index
python full_medial_tire_generator.py --min-n 9 --max-n 9 --dedup --show 5
```
`full_medial_tire_generator.py` is the generator (`generate(n, dedup=True)`
yields `FullMedialTireGraph` objects); `plot_full_medial_tire_n9.py` draws the
atlas.
@@ -0,0 +1,81 @@
0 word=UUUUUUUUU up=9 down=0 bites=-
1 word=UUUUUUDUD up=7 down=2 bites=(6,8)
2 word=UUUUUUDDD up=6 down=3 bites=-
3 word=UUUUUDUUD up=7 down=2 bites=(5,8)
4 word=UUUUUDUDD up=6 down=3 bites=-
5 word=UUUUUDDDD up=5 down=4 bites=-
6 word=UUUUDUUUD up=7 down=2 bites=(4,8)
7 word=UUUUDUUDD up=6 down=3 bites=-
8 word=UUUUDUDUD up=6 down=3 bites=-
9 word=UUUUDUDDD up=5 down=4 bites=-
10 word=UUUUDDUDD up=5 down=4 bites=-
11 word=UUUUDDUDD up=5 down=4 bites=(4,8),(5,7)
12 word=UUUUDDDDD up=4 down=5 bites=-
13 word=UUUUDDDDD up=4 down=5 bites=(4,8)
14 word=UUUDUUUDD up=6 down=3 bites=-
15 word=UUUDUUDUD up=6 down=3 bites=-
16 word=UUUDUUDDD up=5 down=4 bites=-
17 word=UUUDUDUDD up=5 down=4 bites=-
18 word=UUUDUDUDD up=5 down=4 bites=(3,8),(5,7)
19 word=UUUDUDDUD up=5 down=4 bites=-
20 word=UUUDUDDUD up=5 down=4 bites=(3,5),(6,8)
21 word=UUUDUDDDD up=4 down=5 bites=-
22 word=UUUDUDDDD up=4 down=5 bites=(3,5)
23 word=UUUDUDDDD up=4 down=5 bites=(3,8)
24 word=UUUDDUUDD up=5 down=4 bites=-
25 word=UUUDDUUDD up=5 down=4 bites=(3,8),(4,7)
26 word=UUUDDUDDD up=4 down=5 bites=-
27 word=UUUDDUDDD up=4 down=5 bites=(4,6)
28 word=UUUDDUDDD up=4 down=5 bites=(3,8)
29 word=UUUDDDDDD up=3 down=6 bites=-
30 word=UUUDDDDDD up=3 down=6 bites=(3,8)
31 word=UUDUUDUUD up=6 down=3 bites=-
32 word=UUDUUDUDD up=5 down=4 bites=-
33 word=UUDUUDUDD up=5 down=4 bites=(2,8),(5,7)
34 word=UUDUUDDDD up=4 down=5 bites=-
35 word=UUDUUDDDD up=4 down=5 bites=(2,5)
36 word=UUDUDUUDD up=5 down=4 bites=-
37 word=UUDUDUUDD up=5 down=4 bites=(2,8),(4,7)
38 word=UUDUDUDUD up=5 down=4 bites=-
39 word=UUDUDUDUD up=5 down=4 bites=(2,4),(6,8)
40 word=UUDUDUDUD up=5 down=4 bites=(2,8),(4,6)
41 word=UUDUDUDDD up=4 down=5 bites=-
42 word=UUDUDUDDD up=4 down=5 bites=(4,6)
43 word=UUDUDUDDD up=4 down=5 bites=(2,4)
44 word=UUDUDUDDD up=4 down=5 bites=(2,8)
45 word=UUDUDDUDD up=4 down=5 bites=-
46 word=UUDUDDUDD up=4 down=5 bites=(5,7)
47 word=UUDUDDUDD up=4 down=5 bites=(2,4)
48 word=UUDUDDUDD up=4 down=5 bites=(2,8)
49 word=UUDUDDDUD up=4 down=5 bites=-
50 word=UUDUDDDUD up=4 down=5 bites=(6,8)
51 word=UUDUDDDUD up=4 down=5 bites=(2,8)
52 word=UUDUDDDDD up=3 down=6 bites=-
53 word=UUDUDDDDD up=3 down=6 bites=(2,4)
54 word=UUDUDDDDD up=3 down=6 bites=(2,8)
55 word=UUDDUUDDD up=4 down=5 bites=-
56 word=UUDDUUDDD up=4 down=5 bites=(3,6)
57 word=UUDDUDUDD up=4 down=5 bites=-
58 word=UUDDUDUDD up=4 down=5 bites=(5,7)
59 word=UUDDUDUDD up=4 down=5 bites=(2,8)
60 word=UUDDUDDDD up=3 down=6 bites=-
61 word=UUDDUDDDD up=3 down=6 bites=(3,5)
62 word=UUDDUDDDD up=3 down=6 bites=(2,8)
63 word=UUDDDUDDD up=3 down=6 bites=-
64 word=UUDDDUDDD up=3 down=6 bites=(4,6)
65 word=UUDDDUDDD up=3 down=6 bites=(2,8)
66 word=UUDDDUDDD up=3 down=6 bites=(2,8),(3,7),(4,6)
67 word=UDUDUDUDD up=4 down=5 bites=-
68 word=UDUDUDUDD up=4 down=5 bites=(5,7)
69 word=UDUDUDUDD up=4 down=5 bites=(3,5)
70 word=UDUDUDDDD up=3 down=6 bites=-
71 word=UDUDUDDDD up=3 down=6 bites=(3,5)
72 word=UDUDUDDDD up=3 down=6 bites=(1,3)
73 word=UDUDDUDDD up=3 down=6 bites=-
74 word=UDUDDUDDD up=3 down=6 bites=(4,6)
75 word=UDUDDUDDD up=3 down=6 bites=(1,3)
76 word=UDUDDUDDD up=3 down=6 bites=(1,8)
77 word=UDUDDUDDD up=3 down=6 bites=(1,8),(3,7),(4,6)
78 word=UDDUDDUDD up=3 down=6 bites=-
79 word=UDDUDDUDD up=3 down=6 bites=(5,7)
80 word=UDDUDDUDD up=3 down=6 bites=(1,8),(2,4),(5,7)
@@ -0,0 +1,82 @@
# Full vs Reduced Medial Tire Findings
Question: do Definition 3.1 (full medial tire graph) and Definition 3.2
(reduced medial tire graph) differ?
## Experiment
Script:
```bash
python3 papers/medial_tire_decompositions_of_plane_triangulations/experiments/compare_full_reduced_medial_tires.py
```
The script compares two models.
- Ambient tread-face model: medial edges are contributed by annular
triangular faces of the tire tread inside the ambient triangulation.
- Standalone tire-with-boundary-faces model: the outer and inner
boundary walks are also treated as faces, as in the older drawing
script.
## Random Sweep
Command:
```bash
python3 papers/medial_tire_decompositions_of_plane_triangulations/experiments/compare_full_reduced_medial_tires.py
```
Result:
```text
ambient tread-face model
cases checked: 7200
cases where full != reduced: 0
removed-edge reasons: {}
standalone tire-with-boundary-faces model
cases checked: 7200
cases where full != reduced: 7200
removed-edge reasons: {'inner_boundary': 39600, 'outer_boundary': 39600}
first difference:
m=3 k=3 requested_chords=0 seed=0
path=IOOOII chords=[]
full_edges=24 reduced_edges=18
removed examples=[((0, 1), (0, 2)), ((0, 1), (1, 2)), ((0, 2), (1, 2)), ((3, 4), (3, 5)), ((3, 4), (4, 5))]
```
## Exhaustive Small Sweep
Command:
```bash
python3 papers/medial_tire_decompositions_of_plane_triangulations/experiments/compare_full_reduced_medial_tires.py --exhaustive --max-cycle 5 --max-chords 2
```
Result:
```text
exhaustive ambient tread-face model
cases checked: 5578
cases where full != reduced: 0
exhaustive standalone tire-with-boundary-faces model
cases checked: 5578
cases where full != reduced: 5578
first difference:
m=3 k=3 chords=() path=OOOIII
full_edges=24 reduced_edges=18
removed examples=[((0, 1), (0, 2)), ((0, 1), (1, 2)), ((0, 2), (1, 2)), ((3, 4), (3, 5)), ((3, 4), (4, 5))]
```
## Interpretation
For the intended ambient-triangulation definition, the experiments
support the suspicion that Definition 3.1 and Definition 3.2 coincide:
same-boundary medial edges do not arise from annular triangular tread
faces, and inner chords are not incident to tread triangles.
They differ only in the standalone tire-with-boundary-faces model,
where the artificial outer and inner boundary faces create medial edges
between consecutive boundary edges.

Some files were not shown because too many files have changed in this diff Show More