idealpolyhedra / bin /optimize_polyhedron.py
igriv's picture
Add statistical distribution analysis with beta fitting and fix vertex configuration bug
3bf2012
#!/usr/bin/env python3
"""
General-purpose wrapper for optimizing ideal polyhedron volumes.
Usage:
python bin/optimize_polyhedron.py --vertices 7 --trials 10
python bin/optimize_polyhedron.py --vertices 12 --trials 20 --output custom_name.json
"""
import argparse
import json
import numpy as np
import torch
from datetime import datetime
from pathlib import Path
import sys
from ideal_poly_volume_toolkit.geometry import (
delaunay_triangulation_indices,
triangle_volume_from_points_torch,
)
from scipy.optimize import differential_evolution
def spherical_to_complex(theta, phi):
"""Convert spherical coordinates to complex via stereographic projection."""
return np.tan(theta/2) * np.exp(1j * phi)
def compute_volume(params, n_vertices):
"""
Compute volume for a polyhedron with n_vertices.
Fixed vertices to break symmetry:
- z1 = 0
- z2 = 1
- z_∞ = ∞ (implicit in Delaunay triangulation)
Remaining (n_vertices - 3) vertices are parameterized by spherical coords.
"""
# Fixed vertices: 0 and 1 (infinity is implicit)
complex_points = [complex(0, 0), complex(1, 0)]
# Parameterized vertices (2 params each: theta, phi)
n_params = n_vertices - 3
for i in range(n_params):
theta = params[2*i]
phi = params[2*i + 1]
z = spherical_to_complex(theta, phi)
complex_points.append(z)
Z_np = np.array(complex_points, dtype=np.complex128)
# Delaunay triangulation
try:
idx = delaunay_triangulation_indices(Z_np)
except:
return 1000.0 # Penalty for degenerate configuration
# Convert to torch for volume computation
Z_torch = torch.tensor(Z_np, dtype=torch.complex128)
# Compute total volume
total_volume = 0
for (i, j, k) in idx:
try:
vol = triangle_volume_from_points_torch(
Z_torch[i], Z_torch[j], Z_torch[k], series_terms=96
)
total_volume += vol.item()
except:
return 1000.0 # Penalty for invalid configuration
return -total_volume # Negative for minimization
def analyze_structure(Z_np, idx):
"""Analyze the combinatorial structure of the triangulation."""
n_vertices = len(Z_np)
n_faces = len(idx)
# Count edges
edges = set()
for (i, j, k) in idx:
edges.add((min(i,j), max(i,j)))
edges.add((min(i,k), max(i,k)))
edges.add((min(j,k), max(j,k)))
n_edges = len(edges)
# Vertex degrees
vertex_degrees = [0] * n_vertices
for edge in edges:
vertex_degrees[edge[0]] += 1
vertex_degrees[edge[1]] += 1
# Euler characteristic check
euler_char = n_vertices - n_edges + n_faces
return {
'n_vertices': n_vertices,
'n_faces': n_faces,
'n_edges': n_edges,
'euler_characteristic': euler_char,
'vertex_degrees': sorted(vertex_degrees),
}
def reconstruct_vertices(params, n_vertices):
"""Reconstruct complex vertices from parameters."""
complex_points = [complex(0, 0), complex(1, 0)]
n_params = n_vertices - 3
for i in range(n_params):
theta = params[2*i]
phi = params[2*i + 1]
z = spherical_to_complex(theta, phi)
complex_points.append(z)
return np.array(complex_points, dtype=np.complex128)
def main():
parser = argparse.ArgumentParser(
description='Optimize ideal polyhedron volumes',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --vertices 7 --trials 10
%(prog)s --vertices 12 --trials 20 --maxiter 300
%(prog)s --vertices 20 --trials 5 --output results/data/my_20vertex.json
"""
)
parser.add_argument('--vertices', '-v', type=int, required=True,
help='Number of vertices (must be >= 4)')
parser.add_argument('--trials', '-t', type=int, default=10,
help='Number of optimization trials (default: 10)')
parser.add_argument('--maxiter', '-m', type=int, default=200,
help='Max iterations per trial (default: 200)')
parser.add_argument('--popsize', '-p', type=int, default=15,
help='Population size for differential evolution (default: 15)')
parser.add_argument('--output', '-o', type=str, default=None,
help='Output JSON file (default: results/data/{n}vertex_optimization_TIMESTAMP.json)')
parser.add_argument('--seed', '-s', type=int, default=42,
help='Random seed base (default: 42)')
args = parser.parse_args()
if args.vertices < 4:
print("Error: Number of vertices must be at least 4")
sys.exit(1)
# Setup output file
if args.output is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"results/data/{args.vertices}vertex_optimization_{timestamp}.json"
else:
output_file = args.output
# Ensure output directory exists
Path(output_file).parent.mkdir(parents=True, exist_ok=True)
# Calculate number of parameters (2 per free vertex)
n_free_vertices = args.vertices - 3
n_params = n_free_vertices * 2
bounds = [(0.1, np.pi - 0.1), (0, 2*np.pi)] * n_free_vertices
print("=" * 70)
print(f"Ideal Polyhedron Volume Optimization")
print("=" * 70)
print(f"Vertices: {args.vertices}")
print(f"Free vertices: {n_free_vertices} (parameterized)")
print(f"Parameters: {n_params} (spherical coordinates)")
print(f"Trials: {args.trials}")
print(f"Max iterations: {args.maxiter}")
print(f"Population: {args.popsize}")
print(f"Output: {output_file}")
print("=" * 70)
best_volume = 0
best_params = None
all_results = []
for trial in range(args.trials):
print(f"\nTrial {trial + 1}/{args.trials}...")
result = differential_evolution(
lambda p: compute_volume(p, args.vertices),
bounds,
maxiter=args.maxiter,
popsize=args.popsize,
seed=args.seed + trial,
polish=True,
disp=False
)
volume = -result.fun
print(f" Volume: {volume:.8f}")
print(f" Success: {result.success}")
print(f" Iterations: {result.nit}")
all_results.append({
'trial': trial + 1,
'volume': float(volume),
'params': result.x.tolist(),
'success': bool(result.success),
'iterations': int(result.nit),
'function_evals': int(result.nfev)
})
if volume > best_volume:
best_volume = volume
best_params = result.x
print(f" → NEW BEST!")
# Analyze best configuration
print("\n" + "=" * 70)
print("BEST RESULT:")
print("=" * 70)
print(f"Volume: {best_volume:.10f}")
# Reconstruct and analyze
Z_np = reconstruct_vertices(best_params, args.vertices)
idx = delaunay_triangulation_indices(Z_np)
structure = analyze_structure(Z_np, idx)
print(f"\nCombinatorial structure:")
print(f" Vertices: {structure['n_vertices']}")
print(f" Edges: {structure['n_edges']}")
print(f" Faces: {structure['n_faces']}")
print(f" Euler char: {structure['euler_characteristic']} (should be 2 for sphere)")
print(f" Vertex degrees: {structure['vertex_degrees']}")
# Save results
output_data = {
'metadata': {
'timestamp': datetime.now().isoformat(),
'n_vertices': args.vertices,
'n_trials': args.trials,
'maxiter': args.maxiter,
'popsize': args.popsize,
'seed_base': args.seed,
},
'best': {
'volume': float(best_volume),
'params': best_params.tolist(),
'vertices_real': Z_np.real.tolist(),
'vertices_imag': Z_np.imag.tolist(),
'structure': structure,
'triangulation': [list(map(int, tri)) for tri in idx],
},
'all_trials': all_results,
}
with open(output_file, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"\nResults saved to: {output_file}")
print("=" * 70)
if __name__ == '__main__':
main()