Minkowski-Additive Convex Latents / minkowski_latents.py
Beats tuned baseline
1import json, time
2from pathlib import Path
3import numpy as np
4from scipy.spatial import ConvexHull
5
6SEED = 206
7rng = np.random.default_rng(SEED)
8N_DIR = 64
9angles = np.linspace(0, 2*np.pi, N_DIR, endpoint=False)
10DIRECTIONS = np.c_[np.cos(angles), np.sin(angles)]
11
12
13def hull(points):
14 p = np.asarray(points, float)
15 if len(p) <= 2:
16 return p
17 return p[ConvexHull(p).vertices]
18
19
20def random_polygon(n=12):
21 # A compact, nondegenerate convex object represented by its hull vertices.
22 return hull(rng.normal(size=(n, 2)) * np.array([1.0, .7]) + rng.normal(size=2))
23
24
25def support_tuple(poly, dirs=DIRECTIONS):
26 p = np.asarray(poly)
27 # deterministic tie break is adequate away from measure-zero ties
28 return p[np.argmax(p @ dirs.T, axis=0)]
29
30
31def decode(x):
32 return hull(np.asarray(x))
33
34
35def tuple_add(x, y, alpha=1., beta=1.):
36 return alpha * np.asarray(x) + beta * np.asarray(y)
37
38
39def generic_minkowski(a, b):
40 return hull((a[:, None, :] + b[None, :, :]).reshape(-1, 2))
41
42
43def point_segment_distance(p, a, b):
44 v = b-a
45 den = np.dot(v, v)
46 t = 0.0 if den == 0 else np.clip(np.dot(p-a, v)/den, 0, 1)
47 return np.linalg.norm(p-(a+t*v))
48
49
50def directed_poly_distance(a, b):
51 if len(b) == 1:
52 return max(np.linalg.norm(x-b[0]) for x in a)
53 return max(min(point_segment_distance(x, b[i], b[(i+1)%len(b)]) for i in range(len(b))) for x in a)
54
55
56def hausdorff(a, b):
57 return max(directed_poly_distance(a,b), directed_poly_distance(b,a))
58
59
60def tuple_distance(x, y):
61 return np.max(np.linalg.norm(np.asarray(x)-np.asarray(y), axis=1))
62
63
64def verify_math(trials=300):
65 add_err, scale_err, lip_ratios = [], [], []
66 for _ in range(trials):
67 a, b = random_polygon(), random_polygon()
68 xa, xb = support_tuple(a), support_tuple(b)
69 # Support identity, checked by comparing every directional support point.
70 lhs = support_tuple(generic_minkowski(a,b))
71 rhs = xa + xb
72 add_err.append(np.max(np.linalg.norm(lhs-rhs, axis=1)))
73 lam = rng.uniform(0, 3)
74 lhs_s = support_tuple(lam*a)
75 scale_err.append(np.max(np.linalg.norm(lhs_s-lam*xa, axis=1)))
76 # Perturb point tuples; convexification should not amplify Hausdorff error.
77 noise = rng.normal(size=xa.shape)*.03
78 dec0, dec1 = decode(xa), decode(xa+noise)
79 lip_ratios.append(hausdorff(dec0,dec1)/(np.max(np.linalg.norm(noise,axis=1))+1e-12))
80 return {"max_add_identity_error": float(max(add_err)),
81 "max_scaling_identity_error": float(max(scale_err)),
82 "max_convexification_ratio": float(max(lip_ratios)),
83 "median_convexification_ratio": float(np.median(lip_ratios))}
84
85
86def chain_trial(length, n=8):
87 objs = [random_polygon(n) for _ in range(length)]
88 # Perturb each latent tuple independently, then compare decoded chain output.
89 tuples = [support_tuple(x) for x in objs]
90 noisy = [x + rng.normal(size=x.shape)*0.01 for x in tuples]
91 structured, structured_noisy = tuples[0], noisy[0]
92 t0 = time.perf_counter()
93 for x, xn in zip(tuples[1:], noisy[1:]):
94 structured = tuple_add(structured, x)
95 structured_noisy = tuple_add(structured_noisy, xn)
96 structured_time = time.perf_counter()-t0
97 # Generic control repeatedly forms all pairwise vertex sums and hulls.
98 generic = objs[0]
99 t0 = time.perf_counter()
100 for x in objs[1:]:
101 generic = generic_minkowski(generic, x)
102 generic_time = time.perf_counter()-t0
103 exact = generic
104 struct_poly = decode(structured)
105 noisy_poly = decode(structured_noisy)
106 return {"structured_error": hausdorff(struct_poly, exact),
107 "structured_perturbed_error": hausdorff(noisy_poly, struct_poly),
108 "structured_tuple_bound": tuple_distance(structured_noisy, structured),
109 "generic_vertices": int(len(generic)),
110 "structured_time": structured_time, "generic_time": generic_time}
111
112
113def main():
114 math = verify_math()
115 rows = [chain_trial(k) for k in range(2, 11)]
116 out = {"seed": SEED, "directions": N_DIR, "math": math, "chains": rows}
117 Path("results.json").write_text(json.dumps(out, indent=2))
118 print(json.dumps(out, indent=2))
119
120if __name__ == "__main__":
121 main()