idealpolyhedra / examples /analyze_random_triangulation.py
igriv's picture
Add HuggingFace Spaces support
e0ef700
#!/usr/bin/env python3
"""
Generate random points, compute Delaunay triangulation, and analyze optimal angles.
"""
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 fractions import Fraction
from collections import Counter
from ideal_poly_volume_toolkit.rivin_delaunay import check_delaunay_realizability, build_edge_adjacency
def continued_fraction_convergents(x, max_terms=20):
"""Compute convergents of continued fraction expansion."""
convergents = []
a = []
remainder = x
for _ in range(max_terms):
floor_val = int(np.floor(remainder))
a.append(floor_val)
if abs(remainder - floor_val) < 1e-12:
break
remainder = 1.0 / (remainder - floor_val)
p_prev, p_curr = 0, 1
q_prev, q_curr = 1, 0
for ai in a:
p_next = ai * p_curr + p_prev
q_next = ai * q_curr + q_prev
convergents.append((p_next, q_next))
p_prev, p_curr = p_curr, p_next
q_prev, q_curr = q_curr, q_next
return convergents
def analyze_random_configuration(n_vertices=89, seed=42):
"""Generate random configuration and analyze optimal angles."""
print(f"═══════════════════════════════════════════════════════════════")
print(f"RANDOM CONFIGURATION ANALYSIS")
print(f"═══════════════════════════════════════════════════════════════")
print(f"\nNumber of vertices: {n_vertices}")
print(f"Random seed: {seed}")
# Generate random points in unit disk
print(f"\n{'─'*63}")
print(f"STEP 1: Generate random points")
print(f"{'─'*63}")
np.random.seed(seed)
# Generate points in polar coordinates for better distribution
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 in unit disk")
# Compute Delaunay triangulation
print(f"\n{'─'*63}")
print(f"STEP 2: Compute Delaunay triangulation (combinatorics)")
print(f"{'─'*63}")
tri = Delaunay(points)
triangulation = [tuple(sorted(simplex)) for simplex in tri.simplices]
triangulation = sorted(set(triangulation))
print(f"Triangulation computed: {len(triangulation)} triangles")
# Compute optimal angles using Rivin LP
print(f"\n{'─'*63}")
print(f"STEP 3: Compute optimal angles via Rivin LP")
print(f"{'─'*63}")
result = check_delaunay_realizability(triangulation, verbose=False, strict=False)
if not result['realizable']:
print("ERROR: Triangulation not realizable!")
return None
print(f"βœ“ Triangulation is 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 interior edge dihedral angles
print(f"\n{'─'*63}")
print(f"STEP 4: Compute dihedral angles")
print(f"{'─'*63}")
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
# Find best rational approximation
convergents = continued_fraction_convergents(normalized)
if convergents:
best_p, best_q = convergents[-1]
error = abs(normalized - best_p / best_q)
dihedrals.append({
'edge': edge,
'angle_rad': float(dihedral),
'angle_deg': float(np.degrees(dihedral)),
'normalized': float(normalized),
'p': int(best_p),
'q': int(best_q),
'error': float(error),
'rational': f"{best_p}Ο€/{best_q}" if best_q > 1 else f"{best_p}Ο€",
})
print(f"Computed {len(dihedrals)} interior edge dihedrals")
# Analyze rational patterns
print(f"\n{'─'*63}")
print(f"RATIONAL ANGLE ANALYSIS")
print(f"{'─'*63}")
max_denominator = 200
small_error_threshold = 1e-6
# Count denominators with small error
small_q_dihedrals = [d for d in dihedrals if d['q'] <= max_denominator and d['error'] < small_error_threshold]
denominator_counts = Counter(d['q'] for d in small_q_dihedrals)
print(f"\nDenominators q ≀ {max_denominator} with error < {small_error_threshold}:")
print(f"\n{'Denominator':>12} {'Count':>8} {'Relation to n':>20}")
print(f"{'-'*42}")
for q in sorted(denominator_counts.keys()):
count = denominator_counts[q]
relation = ""
if q == n_vertices - 2:
relation = "= n-2"
elif q == n_vertices - 3:
relation = "= n-3"
elif q == n_vertices - 1:
relation = "= n-1"
elif q == n_vertices:
relation = "= n"
print(f" q={q:>3} {count:7d} {relation:>20}")
# Check if ALL angles have small denominators
if len(small_q_dihedrals) == len(dihedrals):
print(f"\nβœ“ ALL {len(dihedrals)} interior edges have rational angles with q ≀ {max_denominator}!")
# Find the dominant denominator
most_common_q = denominator_counts.most_common(1)[0][0]
print(f"\nMost common denominator: q = {most_common_q}", end="")
if most_common_q == n_vertices - 2:
print(f" = n-2 β˜…")
elif most_common_q == n_vertices - 3:
print(f" = n-3 β˜…")
else:
print()
else:
print(f"\n{len(small_q_dihedrals)}/{len(dihedrals)} edges have rational angles")
# Show pattern distribution
print(f"\n{'─'*63}")
print(f"RATIONAL PATTERN DISTRIBUTION")
print(f"{'─'*63}")
pattern_counts = Counter(d['rational'] for d in small_q_dihedrals)
print(f"\n{'Pattern':>10} {'Count':>8} {'Degrees':>12}")
print(f"{'-'*32}")
for pattern, count in pattern_counts.most_common(10):
angle_deg = next(d['angle_deg'] for d in small_q_dihedrals if d['rational'] == pattern)
print(f" {pattern:>8} {count:7d} {angle_deg:11.3f}Β°")
# Sample angles
print(f"\n{'─'*63}")
print(f"SAMPLE DIHEDRAL ANGLES (first 10)")
print(f"{'─'*63}")
print(f"{'Edge':>12} {'Degrees':>10} {'Rational':>12} {'Error':>12}")
print(f"{'-'*48}")
for d in dihedrals[:10]:
print(f" {str(d['edge']):>10} {d['angle_deg']:9.3f}Β° {d['rational']:>12} {d['error']:11.2e}")
# Save results
output_data = {
'n_vertices': n_vertices,
'n_triangles': n_triangles,
'n_interior_edges': len(dihedrals),
'seed': seed,
'vertex_positions': {
'real': vertices_complex.real.tolist(),
'imag': vertices_complex.imag.tolist(),
},
'triangulation': [[int(v) for v in tri] for tri in triangulation],
'denominator_counts': {int(k): int(v) for k, v in denominator_counts.items()},
'all_rational': len(small_q_dihedrals) == len(dihedrals),
}
output_file = Path(f"results/data/{n_vertices}vertex_random_analysis_seed{seed}.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{'─'*63}")
print(f"βœ“ Results saved to: {output_file}")
print(f"{'─'*63}")
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Analyze random triangulation with optimal angles')
parser.add_argument('--vertices', type=int, default=89, help='Number of vertices')
parser.add_argument('--seed', type=int, default=42, help='Random seed')
args = parser.parse_args()
analyze_random_configuration(n_vertices=args.vertices, seed=args.seed)