File size: 5,847 Bytes
82a8f4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import argparse, numpy as np, torch, time
from ideal_poly_volume_toolkit.geometry import (
    delaunay_triangulation_indices,
    triangle_volume_from_points_torch,
)

def random_complex_points(K, rng):
    """Generate K random complex points"""
    # Start with points roughly on unit circle but with some variation
    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 main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--seed', type=int, default=0)
    ap.add_argument('--iters', type=int, default=200)
    ap.add_argument('--series', type=int, default=96)
    ap.add_argument('--print-every', type=int, default=20)
    ap.add_argument('--device', type=str, default='cpu')
    ap.add_argument('--lr', type=float, default=1.0)
    args = ap.parse_args()

    rng = np.random.default_rng(args.seed)
    K = 5  # Five free points
    
    # Initialize with random complex points
    z_init = random_complex_points(K, rng)
    real_parts = torch.tensor(z_init.real, dtype=torch.float64, device=args.device, requires_grad=True)
    imag_parts = torch.tensor(z_init.imag, dtype=torch.float64, device=args.device, requires_grad=True)
    
    print(f"Initial points (besides 0 and 1):")
    for i in range(K):
        print(f"  z[{i+2}] = {real_parts[i].item():.4f} + {imag_parts[i].item():.4f}i")

    # Use LBFGS optimizer on both real and imaginary parts
    opt = torch.optim.LBFGS([real_parts, imag_parts], lr=args.lr, max_iter=20, line_search_fn='strong_wolfe')

    history = []
    triangulation_changes = 0
    prev_triangulation = None
    t0 = time.time()

    for it in range(1, args.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)
            
            # Check if triangulation changed
            if prev_triangulation is not None:
                if idx.shape != prev_triangulation.shape or not np.array_equal(idx, prev_triangulation):
                    triangulation_changes += 1
                    if it <= 10 or it % args.print_every == 0:
                        print(f"  [Triangulation changed at iteration {it}]")
            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, args.series)
            loss = -total  # maximize volume
            loss.backward()
            # Clip gradients to prevent instability
            torch.nn.utils.clip_grad_norm_([real_parts, imag_parts], max_norm=10.0)
            return loss

        opt.step(closure)

        # Log progress
        with torch.no_grad():
            Z_post = build_Z_free(real_parts, imag_parts)
            val_post = torch_sum_volume(Z_post, idx, args.series)
            history.append(float(val_post.item()))
            
            if it % args.print_every == 0 or it in (1, args.iters):
                print(f'\n[{it:03d}] volume ~ {history[-1]:.10f}   (tris={idx.shape[0]})')
                # Print current positions
                for i in range(K):
                    z = complex(real_parts[i].item(), imag_parts[i].item())
                    r, theta = abs(z), np.angle(z) * 180/np.pi
                    print(f'      z[{i+2}] = {z.real:+.4f} {z.imag:+.4f}i  (r={r:.4f}, θ={theta:+.1f}°)')

    t1 = time.time()

    # Final exact evaluation
    with torch.no_grad():
        Zf = build_Z_free(real_parts, imag_parts).detach().cpu().numpy()
        from ideal_poly_volume_toolkit.geometry import ideal_poly_volume_via_delaunay
        vol_exact = ideal_poly_volume_via_delaunay(Zf, mode='eval_only', dps=250)

    print('\n=== Optimization (5 free points) done ===')
    print(f'iters={args.iters}, time={t1-t0:.2f}s')
    print(f'triangulation changes: {triangulation_changes}')
    print(f'initial volume ~ {history[0] if history else 0:.10f}')
    print(f'final fast volume ~ {history[-1]:.10f}')
    print(f'final exact volume  {vol_exact:.12f}')
    
    print('\nFinal configuration:')
    print('  z[0] = 0.0000 + 0.0000i (fixed)')
    print('  z[1] = 1.0000 + 0.0000i (fixed)')
    for i in range(K):
        z = complex(real_parts[i].item(), imag_parts[i].item())
        r, theta = abs(z), np.angle(z) * 180/np.pi
        print(f'  z[{i+2}] = {z.real:+.4f} {z.imag:+.4f}i  (r={r:.4f}, θ={theta:+.1f}°)')
    
    print('\nExpected volumes:')
    print('  Regular tetrahedron: ~1.0149')
    print('  Regular cube: ~5.0747 (5 × tetrahedron)')
    print(f'  Our result: {vol_exact:.4f}')
    
    # Check distances between points
    print('\nPairwise distances:')
    Z_final = build_Z_free(real_parts, imag_parts).detach().numpy()
    for i in range(len(Z_final)):
        for j in range(i+1, len(Z_final)):
            dist = abs(Z_final[i] - Z_final[j])
            if dist > 0.01:  # Skip very small distances
                print(f'  |z[{i}] - z[{j}]| = {dist:.4f}')

if __name__ == '__main__':
    main()