idealpolyhedra / examples /optimization /multiple_optimization_trials.py
igriv's picture
Major reorganization and feature additions
d7d27f0
import numpy as np
import torch
from ideal_poly_volume_toolkit.geometry import (
delaunay_triangulation_indices,
triangle_volume_from_points_torch,
ideal_poly_volume_via_delaunay
)
def random_complex_points(K, rng):
"""Generate K random complex points"""
r = 0.5 + 1.0 * rng.random(K)
theta = 2 * np.pi * rng.random(K)
return r * np.exp(1j * theta)
def build_Z_free(real_parts: torch.Tensor, imag_parts: torch.Tensor) -> torch.Tensor:
"""Build complex array from real and imaginary parts"""
Z = torch.empty(real_parts.numel() + 2, dtype=torch.complex128, device=real_parts.device)
Z[0] = 0 + 0j # Fixed at origin
Z[1] = 1 + 0j # Fixed at 1
Z[2:] = torch.complex(real_parts, imag_parts)
return Z
def torch_sum_volume(Z_t: torch.Tensor, idx, series_terms: int) -> torch.Tensor:
total = torch.zeros((), dtype=torch.float64, device=Z_t.device)
for (i, j, k) in idx:
total = total + triangle_volume_from_points_torch(
Z_t[i], Z_t[j], Z_t[k], series_terms=series_terms
)
return total
def run_optimization_trial(seed, max_iters=100, K=5):
"""Run one optimization trial with given seed"""
rng = np.random.default_rng(seed)
# Initialize with random complex points
z_init = random_complex_points(K, rng)
real_parts = torch.tensor(z_init.real, dtype=torch.float64, requires_grad=True)
imag_parts = torch.tensor(z_init.imag, dtype=torch.float64, requires_grad=True)
# Use LBFGS optimizer
opt = torch.optim.LBFGS([real_parts, imag_parts], lr=1.0, max_iter=20, line_search_fn='strong_wolfe')
prev_triangulation = None
triangulation_changes = 0
for it in range(1, max_iters + 1):
# Rebuild Delaunay triangulation
with torch.no_grad():
Z_np = build_Z_free(real_parts, imag_parts).detach().cpu().numpy()
idx = delaunay_triangulation_indices(Z_np)
if prev_triangulation is not None:
if idx.shape != prev_triangulation.shape or not np.array_equal(idx, prev_triangulation):
triangulation_changes += 1
prev_triangulation = idx.copy()
# Closure for LBFGS
def closure():
opt.zero_grad(set_to_none=True)
Z_t = build_Z_free(real_parts, imag_parts)
total = torch_sum_volume(Z_t, idx, 96)
loss = -total
loss.backward()
torch.nn.utils.clip_grad_norm_([real_parts, imag_parts], max_norm=10.0)
return loss
opt.step(closure)
# Final evaluation
with torch.no_grad():
Zf = build_Z_free(real_parts, imag_parts).detach().cpu().numpy()
vol_fast = torch_sum_volume(build_Z_free(real_parts, imag_parts), idx, 96).item()
vol_exact = ideal_poly_volume_via_delaunay(Zf, mode='eval_only', dps=250)
return {
'seed': seed,
'volume_fast': vol_fast,
'volume_exact': vol_exact,
'final_config': Zf.copy(),
'num_triangles': len(idx),
'triangulation_changes': triangulation_changes
}
# Run multiple trials
print("Running multiple optimization trials with 5 free points...")
print("Expected volumes:")
print(" Regular tetrahedron: ~1.0149")
print(" Regular cube: ~5.0747")
print("\n")
num_trials = 10
results = []
for seed in range(num_trials):
print(f"Trial {seed+1}/{num_trials} (seed={seed})...", end='', flush=True)
result = run_optimization_trial(seed)
results.append(result)
print(f" Volume: {result['volume_exact']:.6f}")
# Analyze results
volumes = [r['volume_exact'] for r in results]
print(f"\n=== Summary of {num_trials} trials ===")
print(f"Minimum volume: {min(volumes):.6f}")
print(f"Maximum volume: {max(volumes):.6f}")
print(f"Average volume: {np.mean(volumes):.6f}")
print(f"Std deviation: {np.std(volumes):.6f}")
# Count how many exceed the cube
cube_volume = 5.0747
num_exceed_cube = sum(1 for v in volumes if v > cube_volume)
print(f"\nTrials exceeding cube volume ({cube_volume:.4f}): {num_exceed_cube}/{num_trials}")
# Group by similar volumes
print("\nVolume distribution:")
volume_groups = {}
for r in results:
v = r['volume_exact']
found = False
for group_v in volume_groups:
if abs(v - group_v) < 0.01: # Group within 0.01
volume_groups[group_v].append(r)
found = True
break
if not found:
volume_groups[v] = [r]
for v in sorted(volume_groups.keys(), reverse=True):
group = volume_groups[v]
print(f" Volume ~{v:.4f}: {len(group)} trial(s) (seeds: {[r['seed'] for r in group]})")
# Check if any special values appear
phi = (1 + np.sqrt(5)) / 2
print(f"\nChecking for special values (φ = {phi:.6f}):")
for v in sorted(volume_groups.keys(), reverse=True):
ratio = v / 1.0149 # Ratio to tetrahedron
print(f" {v:.4f} = {ratio:.4f} × tetrahedron")
# Save the best configuration
best = max(results, key=lambda r: r['volume_exact'])
print(f"\nBest configuration found:")
print(f" Seed: {best['seed']}")
print(f" Volume: {best['volume_exact']:.6f}")
print(f" Points:")
for i, z in enumerate(best['final_config']):
print(f" z[{i}] = {z.real:.4f} + {z.imag:.4f}i")