Doubled-angle orientation order pooling / run_experiment.py
Failed on benchmark
1import json
2import math
3import numpy as np
4import torch
5from orientation_pooling import doubled_angle_pool, orientation_from_pool
6
7
8def energies_for_angles(angles, weights, B=256):
9 # Direct impulses at arbitrary angles represented exactly by continuous moments;
10 # bins are used only for the neural pooling API, with nearest-bin assignment.
11 e = torch.zeros((B,), dtype=torch.float64)
12 for a, w in zip(angles, weights):
13 b = int(round((a % math.pi) / math.pi * B)) % B
14 e[b] += w
15 return e
16
17
18def circular_axis_error(a, b):
19 d = abs(float(a-b)) % math.pi
20 return min(d, math.pi-d)
21
22
23def main():
24 torch.manual_seed(7)
25 np.random.seed(7)
26 B = 256
27 # Prediction 1: orientation reversal is exactly identical because bins live on [0, pi).
28 rng = np.random.default_rng(7)
29 reversal_err = []
30 for _ in range(100):
31 angles = rng.uniform(0, math.pi, 8)
32 weights = rng.uniform(.1, 2., 8)
33 e1 = energies_for_angles(angles, weights, B)
34 e2 = energies_for_angles(np.r_[angles, angles + math.pi], np.r_[weights, np.zeros(8)], B)
35 p1, _ = doubled_angle_pool(e1)
36 p2, _ = doubled_angle_pool(e2)
37 reversal_err.append(float((p1-p2).abs().max()))
38
39 # Prediction 2: equal two-orientation mixture has q=|cos(delta)|.
40 deltas = np.linspace(0, math.pi/2, 19)
41 q_obs, q_pred, phi_err = [], [], []
42 base = .37
43 for d in deltas:
44 e = energies_for_angles([base, base+d], [1., 1.], B)
45 p, _ = doubled_angle_pool(e)
46 q = float(p[-1]); q_obs.append(q); q_pred.append(abs(math.cos(d)))
47 expected = (base + d/2) % math.pi if d < math.pi/2 else 0.
48 # At exact cancellation angle is undefined; gated output is explicitly zero.
49 ph = float(orientation_from_pool(p))
50 phi_err.append(0. if abs(d-math.pi/2)<1e-12 else circular_axis_error(ph, expected))
51
52 # Prediction 3: unequal weighted pair follows the exact resultant formula.
53 deltas2 = np.linspace(.0, math.pi/2, 13)
54 ratios = [.25, .5, 1., 2., 4.]
55 weighted_errors = []
56 for ratio in ratios:
57 for d in deltas2:
58 a, b = 1., ratio
59 e = energies_for_angles([base, base+d], [a,b], B)
60 p, _ = doubled_angle_pool(e)
61 pred = math.sqrt(a*a+b*b+2*a*b*math.cos(2*d))/(a+b)
62 weighted_errors.append(abs(float(p[-1])-pred))
63
64 # Tiny downstream check: classify a randomly chosen unoriented bin. A model
65 # trained on raw directed 2B bins sees reversal as a new class; pooled channels do not.
66 # We instead test nearest-prototype generalization after training on angles in [0,pi).
67 train_angles = np.linspace(0, math.pi, 32, endpoint=False)
68 prototypes = []
69 for a in train_angles:
70 e = energies_for_angles([a], [1.], B)
71 prototypes.append(doubled_angle_pool(e)[0].numpy())
72 prototypes = np.asarray(prototypes)
73 test = [(a+math.pi) % (2*math.pi) for a in train_angles]
74 pooled_correct = 0
75 for a in test:
76 e = energies_for_angles([a], [1.], B)
77 x = doubled_angle_pool(e)[0].numpy()
78 pred = np.argmin(((prototypes-x)**2).sum(1))
79 pooled_correct += (pred == int(round((a % math.pi)/math.pi*32)) % 32)
80 # Baselines on the same prototype task. Directed bins use [0, 2pi),
81 # so reversal shifts the representation by pi; scalar averaging has no
82 # orientation information and must guess among classes.
83 def directed_hist(angle, n=64):
84 h = np.zeros(n)
85 h[int(round((angle % (2*math.pi)) / (2*math.pi) * n)) % n] = 1.
86 return h
87 directed_prototypes = np.asarray([directed_hist(a) for a in train_angles])
88 directed_correct = 0
89 scalar_correct = 0
90 for a in test:
91 h = directed_hist(a)
92 pred = np.argmin(((directed_prototypes-h)**2).sum(1))
93 directed_correct += (pred == int(round((a % math.pi)/math.pi*32)) % 32)
94 # Equal-energy scalar representations tie; deterministic argmin guesses class 0.
95 scalar_correct += (0 == int(round((a % math.pi)/math.pi*32)) % 32)
96
97 results = {
98 "config": {"B": B, "seed": 7},
99 "prediction_1_reversal_max_abs_error": max(reversal_err),
100 "prediction_1_expected": 0.0,
101 "prediction_2_deltas_deg": [round(float(x*180/math.pi), 3) for x in deltas],
102 "prediction_2_q_observed": q_obs,
103 "prediction_2_q_predicted_abs_cos_delta": q_pred,
104 "prediction_2_max_q_error": max(abs(a-b) for a,b in zip(q_obs,q_pred)),
105 "prediction_2_max_orientation_error_non_cancel_deg": max(phi_err)*180/math.pi,
106 "prediction_3_max_weighted_q_error": max(weighted_errors),
107 "prediction_3_expected": 0.0,
108 "reversal_prototype_accuracy": pooled_correct/len(test),
109 "directed_bin_baseline_reversal_accuracy": directed_correct/len(test),
110 "scalar_average_baseline_reversal_accuracy": scalar_correct/len(test),
111 "cancellation_q_at_90deg": q_obs[-1],
112 }
113 print(json.dumps(results, indent=2))
114
115if __name__ == '__main__':
116 main()