idealpolyhedra / examples /save_large_configurations.py
igriv's picture
Add HuggingFace Spaces support
e0ef700
#!/usr/bin/env python3
"""
Generate and save triangulations with optimal angles for various n.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import numpy as np
import json
from datetime import datetime
from scipy.spatial import Delaunay
from ideal_poly_volume_toolkit.rivin_delaunay import check_delaunay_realizability, build_edge_adjacency
from math import gcd
from functools import reduce
def lcm(a, b):
"""Compute least common multiple."""
return abs(a * b) // gcd(a, b)
def analyze_and_save_configuration(n_vertices, seed, output_dir='results/data/large_configs'):
"""Generate, analyze, and save a configuration."""
print(f"\n{'='*70}")
print(f"Configuration: n={n_vertices}, seed={seed}")
print(f"{'='*70}")
# Generate random points
np.random.seed(seed)
radii = np.sqrt(np.random.uniform(0, 1, n_vertices))
angles = np.random.uniform(0, 2*np.pi, n_vertices)
vertices_complex = radii * np.exp(1j * angles)
points = np.column_stack([vertices_complex.real, vertices_complex.imag])
print(f"Generated {n_vertices} random points")
# Compute Delaunay triangulation
tri = Delaunay(points)
triangulation = [tuple(sorted(simplex)) for simplex in tri.simplices]
triangulation = sorted(set(triangulation))
print(f"Triangulation: {len(triangulation)} triangles")
# Get optimal angles from Rivin LP
result = check_delaunay_realizability(triangulation, verbose=False, strict=False)
if not result['realizable']:
print("ERROR: Not realizable!")
return None
print(f"✓ Realizable")
# Extract angles
angles_scaled = result['angles']
angles_radians = angles_scaled * np.pi
n_triangles = len(triangulation)
angles_array = angles_radians.reshape((n_triangles, 3))
# Compute dihedral angles
edge_adjacency = build_edge_adjacency(triangulation)
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
dihedrals.append({
'edge': [int(edge[0]), int(edge[1])],
'angle_radians': float(dihedral),
'angle_degrees': float(np.degrees(dihedral)),
'normalized': float(normalized),
})
print(f"Computed {len(dihedrals)} interior edge dihedrals")
# Analyze rational structure
# Check denominators up to 10*n
max_denom = 10 * n_vertices
denominators_found = set()
for d in dihedrals:
norm = d['normalized']
# Try to find rational approximation
for q in range(1, min(max_denom + 1, 1000)):
p = round(norm * q)
if abs(norm - p/q) < 1e-10:
denominators_found.add(q)
d['rational_p'] = int(p)
d['rational_q'] = int(q)
d['rational_error'] = float(abs(norm - p/q))
break
# Find LCM of all denominators
if denominators_found:
common_denominator = reduce(lcm, denominators_found)
else:
common_denominator = None
print(f"Denominators found: {sorted(denominators_found)}")
print(f"Common denominator (LCM): {common_denominator}")
if common_denominator:
print(f"Ratio q/n: {common_denominator/n_vertices:.3f}")
# Check if all angles are rational with common denominator
all_rational = all('rational_q' in d for d in dihedrals)
if all_rational and common_denominator:
# Verify all are multiples of 1/common_denominator
all_multiples = True
for d in dihedrals:
p = round(d['normalized'] * common_denominator)
error = abs(d['normalized'] - p/common_denominator)
if error > 1e-10:
all_multiples = False
break
if all_multiples:
print(f"✓ ALL {len(dihedrals)} angles are exact multiples of π/{common_denominator}")
# Prepare output data
output_data = {
'metadata': {
'n_vertices': int(n_vertices),
'n_triangles': int(n_triangles),
'n_interior_edges': len(dihedrals),
'seed': int(seed),
'generated': datetime.now().isoformat(),
},
'vertex_positions': {
'real': vertices_complex.real.tolist(),
'imag': vertices_complex.imag.tolist(),
},
'triangulation': [[int(v) for v in tri] for tri in triangulation],
'face_angles': {
'radians': angles_array.tolist(),
'degrees': np.degrees(angles_array).tolist(),
},
'dihedral_angles': dihedrals,
'rational_structure': {
'all_rational': all_rational,
'denominators': sorted(denominators_found),
'common_denominator': int(common_denominator) if common_denominator else None,
'ratio_to_n': float(common_denominator / n_vertices) if common_denominator else None,
}
}
# Save to file
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"n{n_vertices:03d}_seed{seed:03d}_triangulation.json"
with open(output_file, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"✓ Saved to: {output_file}")
return output_data
def main():
"""Generate and save multiple configurations."""
print("═"*70)
print("LARGE CONFIGURATION GENERATOR")
print("Saving triangulations with optimal angles for various n")
print("═"*70)
# Configurations to generate
configs = [
(30, 42),
(40, 42),
(50, 42),
(60, 42),
(70, 42),
(80, 42),
(89, 42),
(100, 42),
(89, 123), # Same n, different seed
(89, 456), # Another seed for n=89
]
results = []
for n, seed in configs:
result = analyze_and_save_configuration(n, seed)
if result:
results.append({
'n': n,
'seed': seed,
'common_denom': result['rational_structure']['common_denominator'],
'ratio': result['rational_structure']['ratio_to_n'],
})
# Create summary
print(f"\n{'='*70}")
print("SUMMARY")
print(f"{'='*70}")
print(f"\n{'n':>4} {'seed':>6} {'q (LCM)':>10} {'q/n':>8}")
print("-"*30)
for r in results:
if r['common_denom']:
print(f"{r['n']:>4} {r['seed']:>6} {r['common_denom']:>10} {r['ratio']:>8.3f}")
else:
print(f"{r['n']:>4} {r['seed']:>6} {'None':>10} {'N/A':>8}")
# Save summary
summary_file = Path('results/data/large_configs/SUMMARY.json')
with open(summary_file, 'w') as f:
json.dump({
'generated': datetime.now().isoformat(),
'configurations': results,
}, f, indent=2)
print(f"\n✓ Summary saved to: {summary_file}")
print(f"\nAll configurations saved to: results/data/large_configs/")
if __name__ == '__main__':
main()