summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.py236
1 files changed, 236 insertions, 0 deletions
diff --git a/tests/test_core.py b/tests/test_core.py
new file mode 100644
index 0000000..50d3aad
--- /dev/null
+++ b/tests/test_core.py
@@ -0,0 +1,236 @@
+import numpy as np
+import torch
+
+from worldalign.common import retrieval_metrics, sliced_wasserstein
+from worldalign.energy import (
+ log_sinkhorn,
+ relation_field_energy,
+ standardized_relation,
+)
+from worldalign.gw import gw_pseudo_targets
+from worldalign.models import Bridge, PrefixAdapter
+from worldalign.vg_diagnose import bundle_signature
+
+
+def test_shapes_and_retrieval():
+ bridge = Bridge(8, 12, hidden_dim=16)
+ prefix = PrefixAdapter(12, 20, prefix_length=4, hidden_dim=16)
+ x = torch.randn(5, 8)
+ y = bridge(x)
+ assert y.shape == (5, 12)
+ assert prefix(y).shape == (5, 4, 20)
+ metrics = retrieval_metrics(y, y)
+ assert metrics["i2t_r@1"] == 1.0
+
+
+def test_sliced_wasserstein_identity():
+ x = torch.randn(32, 16)
+ assert sliced_wasserstein(x, x).item() < 1e-10
+
+
+def test_gw_recovers_structure_without_pairs():
+ rng = np.random.default_rng(7)
+ z = rng.normal(size=(600, 6)).astype(np.float32)
+ q, _ = np.linalg.qr(rng.normal(size=(6, 6)))
+ x = torch.from_numpy(z)
+ y = torch.from_numpy((z @ q).astype(np.float32))[torch.randperm(len(z))]
+ result = gw_pseudo_targets(x, y, clusters=12, seed=3, max_iter=50)
+ assert result["coupling"].shape == (12, 12)
+ assert np.isfinite(result["gw_distance"])
+
+
+def test_bundle_signature_is_view_permutation_and_rotation_invariant():
+ torch.manual_seed(5)
+ x = torch.randn(7, 8, 12)
+ q, _ = torch.linalg.qr(torch.randn(12, 12))
+ permuted = (x @ q)[:, torch.randperm(8)]
+ first = bundle_signature(x)
+ second = bundle_signature(permuted)
+ assert torch.allclose(first, second, atol=2e-5)
+
+
+def test_log_sinkhorn_has_unit_marginals():
+ logits = torch.randn(9, 9)
+ coupling = log_sinkhorn(logits, temperature=0.7, iterations=30)
+ assert torch.allclose(coupling.sum(0), torch.ones(9), atol=1e-4)
+ assert torch.allclose(coupling.sum(1), torch.ones(9), atol=1e-4)
+
+
+def test_transposition_delta_matches_brute_force():
+ from worldalign.manifold_gate import (
+ all_transposition_delta_mse,
+ build_channels,
+ permuted,
+ relation_mse,
+ )
+
+ generator = torch.Generator().manual_seed(11)
+ visual = torch.randn(24, 8, generator=generator)[:, None, :]
+ text = torch.randn(24, 10, generator=generator)[:, None, :]
+ text_channels, _ = build_channels(text, bundle=False)
+ visual_channels, _ = build_channels(visual, bundle=False)
+ delta = all_transposition_delta_mse(text_channels[0], visual_channels[0])
+ base = relation_mse(text_channels[0], visual_channels[0])
+ for p, q in [(0, 1), (3, 17), (5, 23), (10, 11)]:
+ permutation = torch.arange(24)
+ permutation[[p, q]] = permutation[[q, p]]
+ brute = (
+ relation_mse(permuted(text_channels[0], permutation), visual_channels[0])
+ - base
+ )
+ assert torch.allclose(delta[p, q], brute, atol=1e-10)
+ assert torch.allclose(delta[q, p], brute, atol=1e-10)
+
+
+def test_gate_energy_matches_energy_module():
+ from worldalign.manifold_gate import assignment_energy, build_channels
+
+ generator = torch.Generator().manual_seed(3)
+ visual = torch.randn(16, 6, generator=generator)
+ text = torch.randn(16, 9, generator=generator)
+ text_channels, text_relation = build_channels(text[:, None, :], bundle=False)
+ visual_channels, visual_relation = build_channels(visual[:, None, :], bundle=False)
+ permutation = torch.randperm(16, generator=generator)
+ gate = assignment_energy(
+ text_channels, visual_channels, text_relation, visual_relation, permutation
+ )
+ reference_relation, reference_standardized = standardized_relation(visual)
+ mse, kl = relation_field_energy(
+ reference_relation, reference_standardized, text[permutation]
+ )
+ assert abs(gate["mse"] - float(mse)) < 1e-5
+ assert abs(gate["conditional_kl"] - float(kl)) < 1e-4
+
+
+def test_k_derangement_moves_exactly_k():
+ from worldalign.manifold_gate import k_derangement
+
+ generator = torch.Generator().manual_seed(9)
+ for k in (2, 4, 16):
+ permutation = k_derangement(64, k, generator)
+ moved = (permutation != torch.arange(64)).sum()
+ assert int(moved) == k
+
+
+def test_bundle_mean_channel_matches_view_mean_relation():
+ from worldalign.manifold_gate import view_bundle_channels
+
+ torch.manual_seed(4)
+ views = torch.nn.functional.normalize(torch.randn(6, 5, 8), dim=-1)
+ channels = view_bundle_channels(views)
+ assert channels.shape == (4, 6, 6)
+ means = views.double().mean(1)
+ assert torch.allclose(channels[0], means @ means.T, atol=1e-10)
+
+
+def test_batched_transposition_delta_matches_single():
+ from worldalign.manifold_gate import all_transposition_delta_mse
+ from worldalign.view_gate import batched_transposition_delta, standardized_field
+
+ torch.manual_seed(2)
+ views_a = torch.nn.functional.normalize(torch.randn(3, 6, 5), dim=-1)
+ views_b = torch.nn.functional.normalize(torch.randn(3, 6, 7), dim=-1)
+ fields_a = standardized_field(views_a)
+ fields_b = standardized_field(views_b)
+ batched = batched_transposition_delta(fields_a, fields_b)
+ for item in range(3):
+ single = all_transposition_delta_mse(fields_a[item], fields_b[item])
+ assert torch.allclose(batched[item], single, atol=1e-10)
+
+
+def test_batched_descent_reaches_local_minimum():
+ from worldalign.view_gate import (
+ batched_descent,
+ batched_transposition_delta,
+ field_mse,
+ standardized_field,
+ )
+
+ torch.manual_seed(6)
+ visual = torch.nn.functional.normalize(torch.randn(4, 8, 5), dim=-1)
+ noise = torch.nn.functional.normalize(
+ visual + 0.1 * torch.randn(4, 8, 5), dim=-1
+ )
+ visual_fields = standardized_field(visual)
+ text_fields = standardized_field(noise)
+ starts = torch.stack([torch.randperm(8) for _ in range(4)])
+ final_cost, permutations = batched_descent(
+ text_fields.clone(), visual_fields, starts.clone(), sweeps=100
+ )
+ index = torch.arange(4)
+ permuted = text_fields[
+ index[:, None, None], permutations[:, :, None], permutations[:, None, :]
+ ]
+ assert torch.allclose(field_mse(permuted, visual_fields), final_cost, atol=1e-10)
+ delta = batched_transposition_delta(permuted, visual_fields)
+ assert (delta >= -1e-9).all()
+
+
+def test_ricci_flow_returns_finite_symmetric_geometry():
+ import numpy as np
+
+ from worldalign.ricci_control import ollivier_ricci_apsp
+
+ rng = np.random.default_rng(4)
+ points = rng.normal(size=(24, 3))
+ distance = np.linalg.norm(points[:, None] - points[None, :], axis=-1)
+
+ class FlowArgs:
+ neighbors = 5
+ lazy_alpha = 0.5
+ flow_step = 0.4
+
+ flowed = ollivier_ricci_apsp(distance, FlowArgs(), iterations=3)
+ assert np.isfinite(flowed).all()
+ assert np.allclose(flowed, flowed.T, atol=1e-9)
+ assert (np.diag(flowed) == 0).all()
+
+
+def test_paired_relation_field_has_less_energy_than_permutation():
+ generator = torch.Generator().manual_seed(7)
+ visual = torch.randn(32, 12, generator=generator)
+ paired = visual @ torch.randn(12, 20, generator=generator)
+ visual_relation, visual_standardized = standardized_relation(visual)
+ paired_energy = relation_field_energy(
+ visual_relation, visual_standardized, paired
+ )[0]
+ permutation = torch.randperm(32, generator=generator)
+ shuffled_energy = relation_field_energy(
+ visual_relation, visual_standardized, paired[permutation]
+ )[0]
+ assert paired_energy < shuffled_energy
+
+
+def test_tier0_factor_vector_is_one_hot_per_factor():
+ from worldalign.tier0_pipeline import factor_vector
+
+ vector = factor_vector(3, 2, 0, classes=10)
+ assert vector.shape == (17,)
+ assert vector[:10].sum() == 1.0 and vector[3] == 1.0
+ assert vector[10:14].sum() == 1.0 and vector[11] == 1.0
+ assert vector[14:].sum() == 1.0 and vector[14] == 1.0
+
+
+def test_watershed_splits_touching_objects():
+ from worldalign.tier0_pipeline import extract_objects, group_objects
+
+ image = torch.full((3, 64, 64), 0.1)
+ # Two same-coloured discs whose supports touch at a single column.
+ ys, xs = torch.meshgrid(torch.arange(64), torch.arange(64), indexing="ij")
+ for centre in (22, 38):
+ disc = ((ys - 32) ** 2 + (xs - centre) ** 2) <= 8**2
+ image[:, disc] = torch.tensor([0.9, 0.2, 0.2])[:, None]
+ objects = extract_objects(image, peak_distance=4)
+ assert len(objects) == 2
+ groups = group_objects(objects, threshold=0.35)
+ assert len(groups) == 1 and groups[0]["members"] == 2
+
+
+def test_caption_encoding_matches_declared_factors():
+ from worldalign.tier0_pipeline import encode_caption
+
+ ranks = {"red": 0, "blue": 1}
+ states = encode_caption("there are three small red circles, a blue square.", ranks, 2)
+ assert states.shape == (2, 9)
+ assert states[0][0] > 0 and states[0][2 + 2] > 0 and states[0][2 + 4 + 0] > 0
+ assert states[1][1] > 0 and states[1][2 + 0] > 0 and states[1][2 + 4 + 1] > 0