| """
|
| tests/test_dbm.py
|
| =================
|
| Verify DBM (Eqs. 16-17) properties:
|
|
|
| • Gate output ∈ (0, 1)^ρ (Eq. 16, sigmoid)
|
| • Δ shape is (B, d, d) (Eq. 17)
|
| • rank(Δ) ≤ ρ (Proposition 4)
|
| • z=0 → fixed Δ (graceful degradation) (paper §6.3)
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import pytest
|
| import torch
|
|
|
| from caff.dbm import DBM, ContextGate, DBMBlock
|
|
|
|
|
| def test_gate_sigmoid_output_range():
|
| """Eq. 16: g_ℓ ∈ (0, 1)^ρ under sigmoid activation."""
|
| gate = ContextGate(d=64, rho=8, activation="sigmoid")
|
| z = torch.randn(4, 64)
|
| g = gate(z)
|
| assert g.shape == (4, 8)
|
| assert (g > 0).all() and (g < 1).all()
|
|
|
|
|
| def test_gate_relu_ablation():
|
| """Ablation §10.1: ReLU gate produces non-negative output."""
|
| gate = ContextGate(d=64, rho=8, activation="relu")
|
| z = torch.randn(4, 64)
|
| g = gate(z)
|
| assert (g >= 0).all()
|
|
|
|
|
| def test_dbm_shape():
|
| """Eq. 17: Δ_ℓ ∈ ℝ^{d×d} per batch item."""
|
| dbm = DBM(d=64, rho=8)
|
| g = torch.rand(4, 8)
|
| delta = dbm(g)
|
| assert delta.shape == (4, 64, 64)
|
|
|
|
|
| def test_dbm_rank_bound():
|
| """Proposition 4: rank(Δ_ℓ) ≤ ρ.
|
|
|
| With randomly initialized U, V and a strictly-positive gate
|
| (sigmoid output is in (0,1)), rank should be exactly ρ
|
| almost surely.
|
| """
|
| rho = 8
|
| dbm = DBM(d=64, rho=rho)
|
| g = torch.rand(2, rho) * 0.5 + 0.25
|
| delta = dbm(g)
|
| for b in range(delta.shape[0]):
|
| rank = torch.linalg.matrix_rank(delta[b]).item()
|
| assert rank <= rho, f"rank(Δ) = {rank} exceeds ρ = {rho}"
|
|
|
|
|
| def test_dbm_graceful_degradation_at_z_zero():
|
| """Paper §6.3: when z=0, gate becomes σ(b^g_ℓ) — a constant —
|
| and Δ_ℓ collapses to a fixed rank-ρ increment, recovering
|
| DepthBilinear."""
|
| block = DBMBlock(d=64, rho=8)
|
| z_zero_a = torch.zeros(2, 64)
|
| z_zero_b = torch.zeros(2, 64)
|
| delta_a = block(z_zero_a)
|
| delta_b = block(z_zero_b)
|
|
|
| assert torch.allclose(delta_a, delta_b)
|
|
|
| assert torch.allclose(delta_a[0], delta_a[1])
|
|
|
|
|
| def test_dbm_context_sensitivity():
|
| """Different z values produce different Δ matrices."""
|
| block = DBMBlock(d=64, rho=8)
|
| z1 = torch.randn(1, 64)
|
| z2 = torch.randn(1, 64)
|
| delta1 = block(z1)
|
| delta2 = block(z2)
|
| assert not torch.allclose(delta1, delta2)
|
|
|
|
|
| def test_dbm_implements_u_diag_g_v_t():
|
| """Verify literal Eq. 17: Δ = U · diag(g) · V^T (not element-wise gating).
|
|
|
| This guards Failure Mode F1 from the build contract.
|
| """
|
| d, rho = 16, 4
|
| dbm = DBM(d=d, rho=rho)
|
| g = torch.tensor([[0.5, 0.5, 0.5, 0.5]])
|
| delta_module = dbm(g).squeeze(0)
|
|
|
|
|
| delta_ref = dbm.U @ torch.diag(g.squeeze(0)) @ dbm.V.t()
|
| assert torch.allclose(delta_module, delta_ref, atol=1e-5) |