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
|
"""Is the truth the optimum, tied with it, or beaten? Three descents, not seven hundred.
The question that decides the next move is narrow, and the first attempt at it
was not. Suppressing a caption factor leaves the information intact -- the
anchor bound is 0.997 -- while blind matching returns 5.6%, and there are three
possible reasons with three different responses:
E(truth) < E(best found) the searcher simply missed it -> better optimiser
E(truth) == E(best found) the truth is one of many minima -> quotient the symmetry
E(truth) > E(best found) the truth is not the optimum -> information is gone
Answering it does not need a strong searcher. It needs the energy of the truth
and the energy of whatever the standard pipeline actually converges to, which
is one descent per trial rather than the sixty-one the first version ran -- at
N=256 a single descent over all 32,640 transpositions costs minutes, so the
difference is hours against seconds.
"""
from __future__ import annotations
import argparse
import json
import numpy as np
import torch
from .common import write_json
from .spectral_match import grampa
from .synth_fast_gate import ClosedFormEnergy, fast_pair_descent
from .synth_triangle_gate import standardized
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--fields", nargs="+", required=True)
parser.add_argument("--labels", nargs="+", default=None)
parser.add_argument("--trials", type=int, default=3)
parser.add_argument("--iterations", type=int, default=600)
parser.add_argument("--device", default="cuda:3")
parser.add_argument("--tolerance", type=float, default=1e-3)
parser.add_argument("--output", default="artifacts/synth_v1/degeneracy_gate.json")
return parser.parse_args()
def standardise(matrix: np.ndarray) -> np.ndarray:
mask = ~np.eye(len(matrix), dtype=bool)
values = matrix[mask]
out = (matrix - values.mean()) / values.std()
np.fill_diagonal(out, 0.0)
return out
def analyse(path: str, label: str, args: argparse.Namespace) -> dict:
state = torch.load(path, map_location="cpu", weights_only=False)
visual = standardise(state["visual_field"].double().numpy())
text = standardise(state["text_field"].double().numpy())
size = len(visual)
device = torch.device(args.device)
gaps, accuracies, truths = [], [], []
for trial in range(args.trials):
generator = np.random.default_rng(trial)
hidden = generator.permutation(size)
shuffled = text[np.ix_(hidden, hidden)]
shuffled_gpu = standardized(torch.from_numpy(shuffled).to(device)).double()
visual_gpu = standardized(torch.from_numpy(visual).to(device)).double()
energy = ClosedFormEnergy(shuffled_gpu, visual_gpu, 1.0, 0.0, 256)
truth = torch.from_numpy(np.argsort(hidden).copy()).to(device)
truth_energy = float(energy.energy(truth[None])[0])
start = torch.from_numpy(grampa(visual, shuffled, 1.0).copy()).to(device)
final = fast_pair_descent(shuffled_gpu, visual_gpu, start, args.iterations)
found_energy = float(energy.energy(final[None])[0])
# descent from the truth itself: if it moves, the truth is not a local
# minimum either, which separates "not optimal" from "merely not found"
from_truth = fast_pair_descent(
shuffled_gpu, visual_gpu, truth.clone(), args.iterations
)
truth_local = float(energy.energy(from_truth[None])[0])
gaps.append(found_energy - truth_energy)
truths.append(truth_energy - truth_local)
accuracies.append(float((hidden[final.cpu().numpy()] == np.arange(size)).mean()))
gap = float(np.mean(gaps))
scale = abs(float(np.mean([g for g in gaps]))) + 1.0
if gap < -args.tolerance:
verdict = "truth is NOT the optimum -- information deficit"
elif abs(gap) <= args.tolerance:
verdict = "truth is TIED with what is found -- degenerate, quotient the symmetry"
else:
verdict = "truth is DEEPER than what is found -- optimiser failure"
return {
"label": label,
"size": size,
"energy_gap_found_minus_truth": gap,
"truth_descends_further_by": float(np.mean(truths)),
"blind_accuracy": float(np.mean(accuracies)),
"verdict": verdict,
}
def main() -> None:
args = parse_args()
labels = args.labels or [p.split("/")[-1] for p in args.fields]
rows = []
for path, label in zip(args.fields, labels):
row = analyse(path, label, args)
rows.append(row)
print(
f"{row['label']:<20} gap(found-truth)={row['energy_gap_found_minus_truth']:+.4f} "
f"truth-descends={row['truth_descends_further_by']:+.4f} "
f"acc={row['blind_accuracy']:.3f}\n"
f"{'':<20} -> {row['verdict']}",
flush=True,
)
write_json(args.output, {
"protocol": (
"One spectral-start descent per trial, plus one descent started at "
"the truth. A positive gap means the searcher stopped above the "
"truth; zero means it found something the energy cannot "
"distinguish from the truth; negative means the truth is beaten."
),
"rows": rows,
})
print(json.dumps({"done": True}))
if __name__ == "__main__":
main()
|