diff options
| author | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 21:37:55 -0500 |
|---|---|---|
| committer | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 21:37:55 -0500 |
| commit | 22acd2899958def0d103f11da49c2c4a499be773 (patch) | |
| tree | 892b422c1e04cd9854420eda0c965e21df81c8db /worldalign/automorphism_probe.py | |
| parent | ef104fe4f07713bf11f266d2954b7446f176f8ae (diff) | |
The missing term was unary: omit-size solved at its ceiling in 1.4s
Adversarial review of the artifacts found three of my numbers to be
artifacts of my own code. All three reproduced here before acceptance:
- anchor_bound presented probe rows in the same index order on both
sides, so exact twins had their tie broken onto the diagonal. 0.997 ->
0.920 on omit-size. Fixed by scrambling the T-side presentation.
- The truth is not a strict local minimum: 51 transpositions have
exactly zero energy delta. fast_pair_descent only looked stationary
because its break test treats zero as no-improvement.
- scipy's FAQ takes no n_init, so it was swallowed into unknown_options
and 'FAQ x30 restarts' computed bit-identically to plain FAQ. Replaced
with a real restart loop over P0='randomized'.
The blind ceiling for omit-size is 0.836, not 1.0: the text field has 51
exact transposition automorphisms, so T[s,s] is bitwise identical to T
and no objective f(V, P T P^T) can separate an orbit at any order. Every
synthetic accuracy was being divided by the wrong denominator.
The fifteen failed solvers share one property -- all purely quadratic or
purely spectral, none with a node-level term. Eight moments of each
node's own field row, blended with the quadratic term through
Frank-Wolfe, reach 0.837 with an energy gap of exactly zero. The term
must stay in the loop: as a seed for pure-quadratic descent it scores
0.21, pinned through the iterations it scores 0.84 -- which is also why
amplification plateaued, being itself pure-quadratic.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'worldalign/automorphism_probe.py')
| -rw-r--r-- | worldalign/automorphism_probe.py | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/worldalign/automorphism_probe.py b/worldalign/automorphism_probe.py new file mode 100644 index 0000000..46949f4 --- /dev/null +++ b/worldalign/automorphism_probe.py @@ -0,0 +1,96 @@ +"""Exact automorphisms of a relation field, and the blind-recovery ceiling. + +If sigma is an exact automorphism of the text field T (P_sigma T P_sigma^T = T +bit-for-bit) then the blind matching problem cannot distinguish the truth from +truth o sigma: the input (V, T) is literally unchanged. Every functional of +(V, P T P^T) -- the pairwise energy, the third-order tr(M^3) term, any moment +in the ladder, the TLB linear term -- is invariant, so no optimiser and no +relaxation can break the tie. The posterior over the truth is uniform on the +orbit, and the expected accuracy of *any* blind estimator is bounded by + + ceiling = (1/n) * sum_over_orbits |orbit| * (1/|orbit|) + = 1 - (moved - #orbits) / n + +Run: python -m worldalign.automorphism_probe artifacts/synth_v1/omit_size.pt +""" + +from __future__ import annotations + +import math +import sys + +import numpy as np +import torch + + +def standardise(matrix: np.ndarray) -> np.ndarray: + matrix = np.asarray(matrix, dtype=np.float64) + mask = ~np.eye(len(matrix), dtype=bool) + out = (matrix - matrix[mask].mean()) / matrix[mask].std() + np.fill_diagonal(out, 0.0) + return out + + +def automorphic_transpositions(field: np.ndarray, tol: float = 1e-9) -> np.ndarray: + """adj[i, j] iff swapping i and j leaves the field exactly invariant. + + Rows i and j must agree on every coordinate outside {i, j}; the two + excluded coordinates are exactly the ones the swap moves. + """ + size = len(field) + mismatch = np.full((size, size), np.inf) + for i in range(size): + gap = np.abs(field - field[i]) + gap[:, i] = 0.0 + gap[np.arange(size), np.arange(size)] = 0.0 + mismatch[i] = gap.max(axis=1) + np.fill_diagonal(mismatch, np.inf) + return mismatch < tol + + +def orbits(adjacency: np.ndarray) -> list[list[int]]: + size = len(adjacency) + seen = np.zeros(size, dtype=bool) + found: list[list[int]] = [] + for start in range(size): + if seen[start]: + continue + stack = [start] + seen[start] = True + component = [start] + while stack: + node = stack.pop() + for nxt in np.nonzero(adjacency[node] & ~seen)[0]: + seen[nxt] = True + stack.append(nxt) + component.append(int(nxt)) + found.append(sorted(component)) + return [c for c in found if len(c) > 1] + + +def report(field: np.ndarray, name: str) -> float: + size = len(field) + groups = orbits(automorphic_transpositions(field)) + moved = sum(len(c) for c in groups) + log_order = sum(math.lgamma(len(c) + 1) for c in groups) / math.log(10) + ceiling = 1.0 - (moved - len(groups)) / size + print( + f"{name}: {len(groups)} nontrivial orbits covering {moved}/{size} items, " + f"sizes {sorted((len(c) for c in groups), reverse=True)[:12]}, " + f"|Aut| >= 10^{log_order:.1f}" + ) + print(f"{name}: blind accuracy ceiling for any energy-only method = {ceiling:.4f}") + return ceiling + + +def main() -> None: + path = sys.argv[1] if len(sys.argv) > 1 else "artifacts/synth_v1/omit_size.pt" + state = torch.load(path, map_location="cpu", weights_only=False) + visual = standardise(state["visual_field"].double().numpy()) + text = standardise(state["text_field"].double().numpy()) + report(text, "text field") + report(visual, "visual field") + + +if __name__ == "__main__": + main() |
