Symmetry-Quotiented Local Correlation Encoder / orientation_track.py
Beats tuned baseline
1import numpy as np
2
3META = {
4 "name": "orientation_phase_quotient",
5 "domain": "molecular_orientation_symmetry",
6 "description": "Cubic apolar molecular orientations with isotropic/nematic phase labels and nuisance global rotations/sign flips.",
7}
8
9
10def _p2(x):
11 return 0.5 * (3.0 * x * x - 1.0)
12
13
14def _rot(rng):
15 q, r = np.linalg.qr(rng.normal(size=(3, 3)))
16 q = q @ np.diag(np.where(np.diag(r) >= 0, 1.0, -1.0))
17 if np.linalg.det(q) < 0:
18 q[:, 0] *= -1
19 return q
20
21
22def _corr_features(u, L=4, shells=2):
23 a = u.reshape(L, L, L, 3)
24 channels = []
25 for radius in range(1, shells + 1):
26 offsets = [(dx, dy, dz)
27 for dx in range(-radius, radius + 1)
28 for dy in range(-radius, radius + 1)
29 for dz in range(-radius, radius + 1)
30 if (dx, dy, dz) != (0, 0, 0)
31 and dx * dx + dy * dy + dz * dz == radius * radius]
32 c = np.zeros((L, L, L), dtype=np.float64)
33 for dx, dy, dz in offsets:
34 b = np.roll(a, (dx, dy, dz), axis=(0, 1, 2))
35 c += _p2(np.sum(a * b, axis=-1))
36 channels.append(c / len(offsets))
37 return np.stack(channels, axis=0).reshape(-1).astype(np.float32)
38
39
40def _sample(rng, phase, L=4):
41 n = L ** 3
42 if phase == 0:
43 u = rng.normal(size=(n, 3))
44 else:
45 d = rng.normal(size=3)
46 d /= np.linalg.norm(d)
47 u = d + rng.normal(scale=0.28, size=(n, 3))
48 u /= np.linalg.norm(u, axis=1, keepdims=True)
49 u *= rng.choice([-1.0, 1.0], size=(n, 1))
50 # Global frame is nuisance; independent signs are the apolar gauge.
51 return u @ _rot(rng).T
52
53
54def get_dataset(seed, n_train, n_test):
55 rng = np.random.default_rng(int(seed))
56 total = int(n_train) + int(n_test)
57 x, y = [], []
58 for k in range(total):
59 phase = k % 2
60 x.append(_sample(rng, phase))
61 y.append(phase)
62 x = np.asarray(x, dtype=np.float32)
63 y = np.asarray(y, dtype=np.int64)
64 perm = rng.permutation(total)
65 x, y = x[perm], y[perm]
66 return {
67 "xtr": x[:n_train].reshape(n_train, -1),
68 "ytr": y[:n_train],
69 "xte": x[n_train:].reshape(n_test, -1),
70 "yte": y[n_train:],
71 "task": "classification",
72 "metric": "err",
73 "input_shape": (4 * 4 * 4 * 3,),
74 "out_dim": 2,
75 }
76
77
78def invariant_dataset(ds):
79 def conv(x):
80 return np.asarray([_corr_features(v.reshape(4 ** 3, 3)) for v in x], dtype=np.float32)
81 out = dict(ds)
82 out["xtr"] = conv(ds["xtr"])
83 out["xte"] = conv(ds["xte"])
84 out["input_shape"] = (2 * 4 ** 3,)
85 return out
86
87
88def math_check(seed=71):
89 rng = np.random.default_rng(seed)
90 u = _sample(rng, 1)
91 v = u @ _rot(rng).T
92 v *= rng.choice([-1.0, 1.0], size=(len(v), 1))
93 return float(np.max(np.abs(_corr_features(u) - _corr_features(v))))