idealpolyhedra / scripts /generate_arithmeticity_benchmark.py
igriv's picture
Add Rivin-Delaunay optimization and arithmeticity checking
c89947b
#!/usr/bin/env python3
"""
Generate a benchmark JSON file with:
- 1 optimized configuration (ARITHMETIC)
- 9 random configurations (NON-ARITHMETIC)
This demonstrates the Rivin-Delaunay theorem: maximal volume configurations
for a fixed combinatorial triangulation are always arithmetic.
"""
import numpy as np
import json
from datetime import datetime
import sys
sys.path.insert(0, '.')
from ideal_poly_volume_toolkit.geometry import (
delaunay_triangulation_indices,
ideal_poly_volume_via_delaunay,
)
from ideal_poly_volume_toolkit.rivin_delaunay import (
check_delaunay_realizability,
optimize_hyperbolic_volume,
realize_angles_as_points,
)
from ideal_poly_volume_toolkit.rivin_holonomy import (
check_arithmeticity,
check_arithmeticity_from_vertices,
)
def generate_random_sphere_points(n_points, seed=None):
"""Generate n random points uniformly on the unit sphere."""
if seed is not None:
np.random.seed(seed)
points_3d = []
for _ in range(n_points):
vec = np.random.randn(3)
vec = vec / np.linalg.norm(vec)
points_3d.append(vec)
points_3d = np.array(points_3d)
# Stereographic projection from north pole to complex plane
complex_points = []
for x, y, z in points_3d:
if z > 0.9999:
complex_points.append(complex(np.inf, np.inf))
else:
w = complex(x / (1 - z), y / (1 - z))
complex_points.append(w)
return np.array(complex_points)
def generate_random_plane_points(n_points, seed=None, scale=2.0):
"""Generate n random points in the complex plane (truly generic)."""
if seed is not None:
np.random.seed(seed)
# Use Gaussian distribution centered at origin
real_parts = np.random.randn(n_points) * scale
imag_parts = np.random.randn(n_points) * scale
return real_parts + 1j * imag_parts
def optimize_for_fixed_combinatorics(complex_points):
"""Optimize volume for fixed combinatorics using Rivin-Delaunay."""
finite_mask = np.isfinite(complex_points)
finite_points = complex_points[finite_mask]
# Get triangulation
triangulation_indices = delaunay_triangulation_indices(finite_points)
triangles = [tuple(tri) for tri in triangulation_indices]
# Check realizability
realizability = check_delaunay_realizability(triangles, verbose=False)
if not realizability['realizable']:
return None, None, triangles
# Optimize
opt_result = optimize_hyperbolic_volume(
triangles,
initial_angles=realizability['angles_radians'],
verbose=False
)
if not opt_result['success']:
return None, None, triangles
# Realize geometry
realization = realize_angles_as_points(
triangles,
opt_result['angles'],
verbose=False
)
if realization['points'] is not None:
vertex_list = realization['vertex_list']
points_2d = realization['points']
optimized_complex = np.zeros(len(vertex_list), dtype=complex)
for i, v in enumerate(vertex_list):
optimized_complex[v] = complex(points_2d[i, 0], points_2d[i, 1])
return optimized_complex, opt_result['volume'], triangles
return None, None, triangles
def main():
print("Generating Arithmeticity Benchmark")
print("=" * 60)
n_points = 10
base_seed = 42
configs = []
# Generate the first config: optimized (should be arithmetic)
print(f"\nConfig 0: Optimized (Rivin-Delaunay)")
print("-" * 40)
random_points = generate_random_sphere_points(n_points, seed=base_seed)
optimized_points, opt_volume, triangles = optimize_for_fixed_combinatorics(random_points)
if optimized_points is not None:
# Check arithmeticity
arith_result = check_arithmeticity(triangles, verbose=True)
finite_optimized = optimized_points[np.isfinite(optimized_points)]
volume = ideal_poly_volume_via_delaunay(finite_optimized, use_bloch_wigner=True)
config = {
"id": 0,
"type": "optimized",
"description": "Rivin-Delaunay optimized configuration (maximal volume for fixed combinatorics)",
"expected_arithmetic": True,
"n_vertices": int(len(finite_optimized)),
"n_triangles": int(len(triangles)),
"volume": float(volume),
"vertices_real": [float(z.real) for z in finite_optimized],
"vertices_imag": [float(z.imag) for z in finite_optimized],
"triangles": [[int(x) for x in t] for t in triangles],
"holonomy_traces": [float(t) for t in arith_result['traces']],
"is_arithmetic": bool(arith_result['is_arithmetic']),
"seed": int(base_seed),
}
configs.append(config)
print(f" Volume: {volume:.6f}")
print(f" Arithmetic: {arith_result['is_arithmetic']}")
print(f" Traces: {arith_result['n_integral']}/{arith_result['n_generators']} integral")
else:
print(" ERROR: Optimization failed")
# Generate 9 random configs (should be non-arithmetic)
# Use random plane points (not sphere points) to get generic configurations
for i in range(1, 10):
seed = base_seed + i * 100 # Different seeds for variety
print(f"\nConfig {i}: Random (seed={seed})")
print("-" * 40)
# Use random plane points for truly generic configurations
finite_points = generate_random_plane_points(n_points, seed=seed)
if len(finite_points) < 3:
print(" ERROR: Not enough finite points")
continue
# Get triangulation
triangulation_indices = delaunay_triangulation_indices(finite_points)
triangles = [tuple(int(x) for x in tri) for tri in triangulation_indices]
# Check arithmeticity using actual geometry (computes shears from vertices)
arith_result = check_arithmeticity_from_vertices(finite_points, verbose=True)
volume = ideal_poly_volume_via_delaunay(finite_points, use_bloch_wigner=True)
config = {
"id": i,
"type": "random",
"description": f"Random plane configuration (seed={seed})",
"expected_arithmetic": False,
"n_vertices": int(len(finite_points)),
"n_triangles": int(len(triangles)),
"volume": float(volume),
"vertices_real": [float(z.real) for z in finite_points],
"vertices_imag": [float(z.imag) for z in finite_points],
"triangles": [[int(x) for x in t] for t in triangles],
"holonomy_traces": [float(t) for t in arith_result['traces']],
"is_arithmetic": bool(arith_result['is_arithmetic']),
"seed": int(seed),
}
configs.append(config)
print(f" Volume: {volume:.6f}")
print(f" Arithmetic: {arith_result['is_arithmetic']}")
print(f" Traces: {arith_result['n_integral']}/{arith_result['n_generators']} integral")
# Build final benchmark
benchmark = {
"metadata": {
"title": "Arithmeticity Benchmark: Optimized vs Random Configurations",
"description": "Demonstrates that Rivin-Delaunay optimized configurations are arithmetic (all holonomy traces are integers), while random configurations are typically not.",
"created": datetime.now().isoformat(),
"n_vertices": n_points,
"theory": "By Rivin's theorem, the maximal volume configuration for a fixed combinatorial triangulation is unique and arithmetic. Random configurations do not achieve this maximum and are generically non-arithmetic.",
},
"summary": {
"total_configs": len(configs),
"optimized_configs": sum(1 for c in configs if c['type'] == 'optimized'),
"random_configs": sum(1 for c in configs if c['type'] == 'random'),
"arithmetic_count": sum(1 for c in configs if c['is_arithmetic']),
"non_arithmetic_count": sum(1 for c in configs if not c['is_arithmetic']),
},
"configurations": configs,
}
# Save
output_file = "arithmeticity_benchmark.json"
with open(output_file, 'w') as f:
json.dump(benchmark, f, indent=2)
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"Total configurations: {len(configs)}")
print(f"Arithmetic: {benchmark['summary']['arithmetic_count']}")
print(f"Non-arithmetic: {benchmark['summary']['non_arithmetic_count']}")
print(f"\nSaved to: {output_file}")
# Verify: optimized should be arithmetic, randoms should (mostly) not be
optimized_configs = [c for c in configs if c['type'] == 'optimized']
random_configs = [c for c in configs if c['type'] == 'random']
print("\nVerification:")
if all(c['is_arithmetic'] for c in optimized_configs):
print(" [PASS] All optimized configs are arithmetic")
else:
print(" [FAIL] Some optimized configs are NOT arithmetic!")
non_arith_randoms = sum(1 for c in random_configs if not c['is_arithmetic'])
print(f" Random configs: {non_arith_randoms}/{len(random_configs)} are non-arithmetic")
if __name__ == "__main__":
main()