summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-08-01 17:59:32 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-08-01 17:59:32 -0500
commit4f7ee05cc3b072478062e53645af016861c4b529 (patch)
treee7dc4e428f3dbe199b40a23be49019846430128e /tests
parentf29c41e78da10d3c40afdd2deeb43c4d73f5eb43 (diff)
The failure is the optimiser, not the information: a 257x faster descent and a benchmark
The gate settles which failure mode each field is in, and refutes the symmetry hypothesis I proposed. On the caption-omitted field the truth is a STRICT local minimum -- descent started at the truth does not move at all -- the anchor bound says the information is 99.7% intact, and our solver stops 0.44 above it at 4.9% accuracy. That is a pure optimiser failure. Natural data is the opposite: descent from the truth falls a further 0.167, so the truth is not even locally optimal, which is the information-deficit signature the 0.291 bound predicted. Steepest descent was brute-forcing all 32,640 candidate permutations through the full energy every step, including a batched cube trace with the triangle term active -- 203 seconds per descent, which is why the gates were hopeless. The pairwise term needs one matrix product for the whole table: swapping p,q changes the alignment sum by 2(C_pq + C_qp - C_pp - C_qq + 2 A_pq B_pq) with C = A @ B. Verified against brute force to 1e-9 before use, and the fast descent reaches the same optimum. 203s -> 0.79s. Adds a matching benchmark with known-reachable answers and the solver families never tried on these fields: Gromov-Wasserstein, entropic GW with an annealed regulariser, BAPG. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.py57
1 files changed, 57 insertions, 0 deletions
diff --git a/tests/test_core.py b/tests/test_core.py
index 09e58f1..429eed0 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -323,3 +323,60 @@ def test_structured_phrase_encoding_separates_head_from_modifier():
np.mean([vectors[t] for t in ["red", "bus"]], axis=0),
np.mean([vectors[t] for t in ["bus", "red"]], axis=0),
)
+
+
+def test_closed_form_pair_swap_deltas_match_brute_force():
+ """Every transposition's energy change, against the definition."""
+ import numpy as np
+ from worldalign.synth_fast_gate import (
+ ClosedFormEnergy, all_pair_swap_deltas, all_swaps, apply_swaps,
+ )
+
+ size = 24
+ generator = torch.Generator().manual_seed(0)
+ def field():
+ raw = torch.randn(size, size, generator=generator, dtype=torch.float64)
+ out = (raw + raw.T) / 2
+ out.fill_diagonal_(0.0)
+ return out
+
+ text, visual = field(), field()
+ energy = ClosedFormEnergy(text, visual, 1.0, 0.0, 64)
+ permutation = torch.randperm(size, generator=generator)
+
+ base = float(energy.energy(permutation[None])[0])
+ swaps = all_swaps(size, torch.device("cpu"))
+ brute = energy.energy(apply_swaps(permutation, swaps)) - base
+
+ permuted = text[permutation[:, None], permutation[None, :]]
+ table = all_pair_swap_deltas(permuted, visual)
+ closed = table[swaps[:, 0], swaps[:, 1]]
+
+ assert torch.allclose(closed, brute, atol=1e-9), (
+ closed[:4].tolist(), brute[:4].tolist()
+ )
+
+
+def test_fast_pair_descent_matches_brute_force_descent():
+ """The fast descent must reach the same energy as the slow one."""
+ from worldalign.synth_fast_gate import (
+ ClosedFormEnergy, all_swaps, fast_pair_descent, steepest_descent,
+ )
+
+ size = 24
+ generator = torch.Generator().manual_seed(1)
+ def field():
+ raw = torch.randn(size, size, generator=generator, dtype=torch.float64)
+ out = (raw + raw.T) / 2
+ out.fill_diagonal_(0.0)
+ return out
+
+ text, visual = field(), field()
+ energy = ClosedFormEnergy(text, visual, 1.0, 0.0, 64)
+ start = torch.randperm(size, generator=generator)
+ slow, slow_value = steepest_descent(
+ energy, start.clone(), all_swaps(size, torch.device("cpu")), 500
+ )
+ fast = fast_pair_descent(text, visual, start.clone(), 500)
+ fast_value = float(energy.energy(fast[None])[0])
+ assert abs(fast_value - slow_value) < 1e-9, (fast_value, slow_value)