idealpolyhedra / examples /extract_combinatorics.py
igriv's picture
Add HuggingFace Spaces support
e0ef700
#!/usr/bin/env python3
"""
Extract combinatorics and optimal angles from maximal volume configuration.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import numpy as np
import json
from scipy.spatial import Delaunay
from ideal_poly_volume_toolkit.rivin_delaunay import check_delaunay_realizability, build_edge_adjacency
def extract_combinatorics_and_angles(json_file):
"""Extract triangulation and optimal angles from optimization result."""
# Load configuration
with open(json_file, 'r') as f:
data = json.load(f)
# Get vertex positions
real = np.array(data['best']['vertices_real'])
imag = np.array(data['best']['vertices_imag'])
points = np.column_stack([real, imag])
n_vertices = len(points)
volume = data['best']['volume']
print(f"═══════════════════════════════════════════════════════════════")
print(f"MAXIMAL VOLUME IDEAL POLYHEDRON - COMPLETE ANALYSIS")
print(f"═══════════════════════════════════════════════════════════════")
print(f"\nVolume: {volume:.12f}")
print(f"Vertices: {n_vertices}")
# Compute Delaunay triangulation
tri = Delaunay(points)
triangulation = [tuple(sorted(simplex)) for simplex in tri.simplices]
triangulation = sorted(set(triangulation)) # Remove duplicates, sort
print(f"Triangles: {len(triangulation)}")
# Display triangulation
print(f"\n{'─'*63}")
print(f"TRIANGULATION (Combinatorial Structure)")
print(f"{'─'*63}")
print("\nTriangles (vertex indices):")
for i, tri in enumerate(triangulation, 1):
print(f" {i:2d}. {tri}")
# Get optimal angles from Rivin LP
print(f"\n{'─'*63}")
print(f"OPTIMAL ANGLES FROM RIVIN LP")
print(f"{'─'*63}")
result = check_delaunay_realizability(triangulation, verbose=False, strict=False)
if not result['realizable']:
print("ERROR: Configuration not realizable!")
return
# Extract angles (convert from scaled units to radians)
angles_scaled = result['angles']
angles_radians = angles_scaled * np.pi
n_triangles = len(triangulation)
angles_array = angles_radians.reshape((n_triangles, 3))
print("\nFace angles (interior angles of each triangle):")
print(f"{'Tri#':>5} {'Vertices':>20} {'Vtx':>5} {'Angle (rad)':>14} {'Angle (deg)':>12} {'Angle/Ο€':>10}")
print(f"{'-'*70}")
for i, tri in enumerate(triangulation):
for j, vertex in enumerate(tri):
angle_rad = angles_array[i, j]
angle_deg = np.degrees(angle_rad)
angle_normalized = angle_rad / np.pi
tri_str = f"({tri[0]},{tri[1]},{tri[2]})"
print(f" {i+1:3d}. {tri_str:>18s} v{vertex:2d} {angle_rad:13.10f} {angle_deg:11.6f}Β° {angle_normalized:9.6f}Ο€")
# Compute ALL dihedral angles
print(f"\n{'='*78}")
print(f"ALL DIHEDRAL ANGLES (Complete Ideal Polyhedron)")
print(f"{'='*78}")
from scipy.spatial import ConvexHull
from fractions import Fraction
# Find boundary (convex hull)
hull = ConvexHull(points)
boundary_edges = set()
boundary_vertices = set()
for simplex in hull.simplices:
edge = tuple(sorted([simplex[0], simplex[1]]))
boundary_edges.add(edge)
boundary_vertices.add(simplex[0])
boundary_vertices.add(simplex[1])
edge_adjacency = build_edge_adjacency(triangulation)
interior_edges = [e for e, ops in edge_adjacency.items() if len(ops) == 2]
print(f"\nTopology:")
print(f" Total vertices: {n_vertices}")
print(f" Finite triangles: {len(triangulation)}")
print(f" Boundary vertices: {len(boundary_vertices)}")
print(f" Interior edges: {len(interior_edges)}")
print(f" Boundary edges: {len(boundary_edges)}")
print(f" Total edges: {len(interior_edges)} + {len(boundary_edges)} + {len(boundary_vertices)} = {len(interior_edges) + len(boundary_edges) + len(boundary_vertices)}")
print(f" Expected (3V-6): {3*n_vertices - 6}")
# TYPE 1: Interior edge dihedrals
print(f"\n{'─'*78}")
print(f"TYPE 1: Interior Edge Dihedrals ({len(interior_edges)} edges)")
print(f"(Edges shared by two finite triangles)")
print(f"{'─'*78}")
all_dihedrals = []
for edge, opposite_corners in sorted(edge_adjacency.items()):
if len(opposite_corners) == 2:
angle1 = angles_array[opposite_corners[0][0], opposite_corners[0][1]]
angle2 = angles_array[opposite_corners[1][0], opposite_corners[1][1]]
dihedral = angle1 + angle2
normalized = dihedral / np.pi
frac = Fraction(normalized).limit_denominator(20)
rational = f"{frac.numerator}Ο€/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο€"
all_dihedrals.append({
'type': 'interior',
'edge': edge,
'dihedral_rad': dihedral,
'dihedral_deg': np.degrees(dihedral),
'normalized': normalized,
'rational': rational,
'p': frac.numerator,
'q': frac.denominator,
})
# Print first 10
for d in all_dihedrals[:10]:
print(f" {str(d['edge']):>12} {d['dihedral_deg']:7.3f}Β° = {d['normalized']:8.6f}Ο€ β‰ˆ {d['rational']:>8}")
if len(all_dihedrals) > 10:
print(f" ... ({len(all_dihedrals) - 10} more)")
# TYPE 2: Boundary edge dihedrals
print(f"\n{'─'*78}")
print(f"TYPE 2: Boundary Edge Dihedrals ({len(boundary_edges)} edges)")
print(f"(Angle opposite each boundary edge in the triangle containing it)")
print(f"{'─'*78}")
boundary_dihedrals = []
for edge in sorted(boundary_edges):
# Find the triangle containing this edge
v1, v2 = edge
for i, tri in enumerate(triangulation):
if v1 in tri and v2 in tri:
# Find the third vertex (opposite to the edge)
v3 = [v for v in tri if v != v1 and v != v2][0]
v3_idx = tri.index(v3)
dihedral = angles_array[i, v3_idx]
normalized = dihedral / np.pi
frac = Fraction(normalized).limit_denominator(20)
rational = f"{frac.numerator}Ο€/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο€"
boundary_dihedrals.append({
'type': 'boundary',
'edge': edge,
'dihedral_rad': dihedral,
'dihedral_deg': np.degrees(dihedral),
'normalized': normalized,
'rational': rational,
'p': frac.numerator,
'q': frac.denominator,
})
print(f" {str(edge):>12} {np.degrees(dihedral):7.3f}Β° = {normalized:8.6f}Ο€ β‰ˆ {rational:>8}")
break
all_dihedrals.extend(boundary_dihedrals)
# TYPE 3: Vertex-to-∞ dihedrals
print(f"\n{'─'*78}")
print(f"TYPE 3: Vertex-to-∞ Dihedrals ({len(boundary_vertices)} edges)")
print(f"(Sum of all angles at each boundary vertex)")
print(f"{'─'*78}")
vertex_dihedrals = []
for v in sorted(boundary_vertices):
# Sum all angles at this vertex
total_angle = 0
for i, tri in enumerate(triangulation):
if v in tri:
v_idx = tri.index(v)
total_angle += angles_array[i, v_idx]
normalized = total_angle / np.pi
frac = Fraction(normalized).limit_denominator(20)
rational = f"{frac.numerator}Ο€/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο€"
vertex_dihedrals.append({
'type': 'to_infinity',
'edge': (v, 'inf'),
'dihedral_rad': total_angle,
'dihedral_deg': np.degrees(total_angle),
'normalized': normalized,
'rational': rational,
'p': frac.numerator,
'q': frac.denominator,
})
print(f" v{v}β†’βˆž {np.degrees(total_angle):11.3f}Β° = {normalized:8.6f}Ο€ β‰ˆ {rational:>8} {'< Ο€ βœ“' if total_angle < np.pi else 'β‰₯ Ο€ βœ—'}")
all_dihedrals.extend(vertex_dihedrals)
dihedrals = all_dihedrals # For later use
# Summary of rational patterns
print(f"\n{'─'*63}")
print(f"RATIONAL ANGLE SUMMARY")
print(f"{'─'*63}")
from collections import Counter
pattern_counts = Counter(d['rational'] for d in dihedrals)
print(f"\n{'Pattern':>10} {'Count':>8} {'Degrees':>12}")
print(f"{'-'*32}")
for pattern, count in pattern_counts.most_common():
# Get representative angle
angle_deg = next(d['dihedral_deg'] for d in dihedrals if d['rational'] == pattern)
print(f" {pattern:>8} {count:7d} {angle_deg:11.3f}Β°")
# Determine the dominant denominator
denominator_counts = Counter(d['q'] for d in dihedrals)
dominant_q = denominator_counts.most_common(1)[0][0]
print(f"\n{'─'*63}")
if len(denominator_counts) == 1 and dominant_q > 1:
print(f"VERIFICATION: All angles are exact multiples of Ο€/{dominant_q}!")
else:
print(f"VERIFICATION: All angles are exact rational multiples of Ο€!")
print(f"{'─'*63}")
# Export to JSON
output_data = {
'metadata': {
'source_file': str(json_file),
'volume': float(volume),
'n_vertices': int(n_vertices),
'n_triangles': len(triangulation),
},
'combinatorics': {
'triangles': [[int(v) for v in tri] for tri in triangulation],
},
'optimal_angles': {
'face_angles_radians': angles_array.tolist(),
'face_angles_degrees': np.degrees(angles_array).tolist(),
'dihedral_angles': [
{
'type': d['type'],
'edge': [int(d['edge'][0]), str(d['edge'][1])], # Handle 'inf' for vertex-to-∞
'angle_radians': float(d['dihedral_rad']),
'angle_degrees': float(d['dihedral_deg']),
'rational_form': d['rational'],
'p': int(d['p']),
'q': int(d['q']),
}
for d in dihedrals
],
},
'vertex_positions': {
'real': real.tolist(),
'imag': imag.tolist(),
},
}
# Generate output filename based on input file
input_name = Path(json_file).stem
output_file = Path(f'results/data/{input_name}_combinatorics.json')
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"\nβœ“ Exported to: {output_file}")
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Extract combinatorics and optimal angles from maximal volume config')
parser.add_argument('json_file', help='Path to optimization result JSON file')
args = parser.parse_args()
extract_combinatorics_and_angles(args.json_file)