File size: 6,896 Bytes
e0ef700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env python3
"""
Debug the zero spanning tree cases - these shouldn't exist for 3-connected graphs!
"""

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))

import json
import numpy as np
import networkx as nx
from ideal_poly_volume_toolkit.plantri_interface import find_plantri_executable
import subprocess

def get_triangulations_text(n_vertices: int, min_connectivity: int = 3) -> list:
    """Generate triangulations in ASCII format."""
    plantri = find_plantri_executable()
    if plantri is None:
        raise RuntimeError("plantri not found")

    args = [plantri, f'-pc{min_connectivity}', '-a', str(n_vertices)]
    result = subprocess.run(args, capture_output=True, text=True)

    triangulations = []

    for line in result.stdout.split('\n'):
        line = line.strip()
        if not line or line.startswith('>'):
            continue

        parts = line.split(maxsplit=1)
        if len(parts) != 2:
            continue

        n = int(parts[0])
        adj_str = parts[1]

        # Build adjacency dict
        adj = {}
        vertex_lists = adj_str.split(',')

        for v_idx, neighbor_str in enumerate(vertex_lists):
            neighbors = []
            for letter in neighbor_str:
                neighbor_idx = ord(letter) - ord('a')
                neighbors.append(neighbor_idx)
            adj[v_idx] = neighbors

        # Convert adjacency to triangles
        triangles = []
        for v0 in range(n):
            for v1 in adj[v0]:
                if v1 <= v0:
                    continue
                for v2 in adj[v1]:
                    if v2 <= v1:
                        continue
                    if v2 in adj[v0]:
                        tri = tuple(sorted([v0, v1, v2]))
                        if tri not in triangles:
                            triangles.append(tri)

        if triangles:
            triangulations.append((n, adj, triangles))

    return triangulations


def remove_vertex_to_planar(triangles: list, vertex_to_remove: int) -> list:
    """Remove a vertex to create planar triangulation."""
    return [tri for tri in triangles if vertex_to_remove not in tri]


def triangles_to_graph(triangles: list) -> nx.Graph:
    """Convert triangle list to NetworkX graph."""
    G = nx.Graph()
    for tri in triangles:
        v0, v1, v2 = tri
        G.add_edge(v0, v1)
        G.add_edge(v1, v2)
        G.add_edge(v2, v0)
    return G


def count_spanning_trees_kirchhoff(G: nx.Graph) -> int:
    """Count spanning trees using Kirchhoff's matrix-tree theorem."""
    if len(G.nodes()) == 0:
        return 0
    if len(G.nodes()) == 1:
        return 1

    # Get Laplacian matrix
    L = nx.laplacian_matrix(G).toarray()

    # Remove first row and column (compute cofactor)
    L_reduced = L[1:, 1:]

    # Compute determinant (this is the number of spanning trees)
    det = np.linalg.det(L_reduced)

    # Round to nearest integer (should be exact integer, but floating point)
    return int(round(det))


def debug_zero_cases():
    """Debug cases with zero spanning trees."""

    print("="*70)
    print("DEBUGGING ZERO SPANNING TREE CASES")
    print("="*70)

    # Generate n=10 triangulations
    print("\nGenerating n=10 triangulations...")
    closed_tris = get_triangulations_text(10, min_connectivity=3)
    print(f"Generated {len(closed_tris)} closed triangulations")

    # Find first few zero cases
    zero_cases = []
    for idx, (n, adj, closed_tri) in enumerate(closed_tris):
        planar_tri = remove_vertex_to_planar(closed_tri, 0)
        G = triangles_to_graph(planar_tri)
        n_spanning = count_spanning_trees_kirchhoff(G)

        if n_spanning == 0:
            zero_cases.append((idx, n, adj, closed_tri, planar_tri, G))
            if len(zero_cases) >= 5:
                break

    print(f"\nFound {len(zero_cases)} zero-spanning-tree cases in first {idx+1} triangulations")

    # Analyze each zero case
    for case_num, (idx, n, adj, closed_tri, planar_tri, G) in enumerate(zero_cases, 1):
        print("\n" + "="*70)
        print(f"ZERO CASE #{case_num}: Triangulation index {idx}")
        print("="*70)

        # Original closed triangulation
        print(f"\nOriginal closed triangulation (n={n} vertices):")
        print(f"  Number of triangles: {len(closed_tri)}")
        print(f"  Triangles: {closed_tri[:10]}..." if len(closed_tri) > 10 else f"  Triangles: {closed_tri}")

        # Build graph from closed triangulation
        G_closed = nx.Graph()
        for v in range(n):
            for neighbor in adj[v]:
                G_closed.add_edge(v, neighbor)

        print(f"\nClosed graph properties:")
        print(f"  Nodes: {G_closed.number_of_nodes()}")
        print(f"  Edges: {G_closed.number_of_edges()}")
        print(f"  Connected: {nx.is_connected(G_closed)}")
        print(f"  Node connectivity: {nx.node_connectivity(G_closed)}")

        # Check degrees
        degrees = dict(G_closed.degree())
        print(f"  Degree sequence: {sorted(degrees.values())}")

        # Planar triangulation (after removing vertex 0)
        print(f"\nPlanar triangulation (after removing vertex 0):")
        print(f"  Number of triangles: {len(planar_tri)}")
        print(f"  Triangles: {planar_tri[:10]}..." if len(planar_tri) > 10 else f"  Triangles: {planar_tri}")

        # Planar graph
        print(f"\nPlanar graph properties:")
        print(f"  Nodes: {G.number_of_nodes()}")
        print(f"  Edges: {G.number_of_edges()}")
        print(f"  Connected: {nx.is_connected(G)}")

        if not nx.is_connected(G):
            components = list(nx.connected_components(G))
            print(f"  Number of components: {len(components)}")
            print(f"  Component sizes: {[len(c) for c in components]}")
            print(f"  Components: {components}")

        # Check which vertices appear in planar triangulation
        vertices_in_planar = set()
        for tri in planar_tri:
            vertices_in_planar.update(tri)

        print(f"\nVertices analysis:")
        print(f"  Vertices in planar triangulation: {sorted(vertices_in_planar)}")
        print(f"  Expected vertices (1 to {n-1}): {list(range(1, n))}")
        missing = set(range(1, n)) - vertices_in_planar
        if missing:
            print(f"  MISSING vertices: {sorted(missing)}")

        # Check Laplacian
        if G.number_of_nodes() > 0:
            L = nx.laplacian_matrix(G).toarray()
            print(f"\nLaplacian matrix rank: {np.linalg.matrix_rank(L)}")
            print(f"Expected rank for connected graph: {G.number_of_nodes() - 1}")

            eigenvalues = np.linalg.eigvalsh(L)
            print(f"Laplacian eigenvalues (sorted): {eigenvalues[:5]}...")
            print(f"Number of zero eigenvalues: {sum(abs(ev) < 1e-10 for ev in eigenvalues)}")


if __name__ == '__main__':
    debug_zero_cases()