"""Smoke/contract tests for tactile_vae.model.tactile_vae.""" from pathlib import Path import sys import tempfile import torch _REPO_ROOT = Path(__file__).resolve().parents[2] if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) from tactile_vae.model.tactile_vae import TactileVAE, VAELoss, load_pretrained def _small_model() -> TactileVAE: return TactileVAE( img_size=64, patch_size=16, in_chans=3, embed_dim=96, encoder_depth=2, encoder_heads=4, decoder_embed_dim=96, decoder_depth=2, decoder_heads=4, latent_dim=48, ) def test_forward_contract_shapes(): torch.manual_seed(0) model = _small_model() x = torch.randn(2, 3, 64, 64) out = model(x) assert out["x_hat"].shape == x.shape assert out["mu"].shape == (2, 48) assert out["logvar"].shape == (2, 48) assert out["z"].shape == (2, 48) assert out["pred_patches"].shape == (2, (64 // 16) ** 2, 16 * 16 * 3) def test_losses_finite_and_backprop(): torch.manual_seed(0) model = _small_model() criterion = VAELoss(beta=1e-3, recon_type="l1", ssim_weight=0.1) x = torch.randn(2, 3, 64, 64) out = model(x) losses = criterion(out["x_hat"], x, out["mu"], out["logvar"]) for name, val in losses.items(): assert torch.isfinite(val).all(), f"non-finite loss component: {name}" losses["total"].backward() grad_ok = any(p.grad is not None and torch.isfinite(p.grad).all() for p in model.parameters() if p.requires_grad) assert grad_ok, "expected finite gradients after backward" def test_tiny_overfit_recon_drops(): torch.manual_seed(0) model = _small_model() criterion = VAELoss(beta=1e-3, recon_type="l1", ssim_weight=0.0) opt = torch.optim.Adam(model.parameters(), lr=1e-3) x = torch.tanh(torch.randn(8, 3, 64, 64)) with torch.no_grad(): out0 = model(x) start = criterion(out0["x_hat"], x, out0["mu"], out0["logvar"])["recon_total"].item() for _ in range(30): out = model(x) losses = criterion(out["x_hat"], x, out["mu"], out["logvar"]) opt.zero_grad(set_to_none=True) losses["total"].backward() opt.step() with torch.no_grad(): out1 = model(x) end = criterion(out1["x_hat"], x, out1["mu"], out1["logvar"])["recon_total"].item() assert end < start, f"expected recon to decrease; start={start:.4f}, end={end:.4f}" def test_checkpoint_roundtrip_consistent_eval(): torch.manual_seed(0) model = _small_model().eval() x = torch.randn(2, 3, 64, 64) with torch.no_grad(): out_ref = model(x, sample=False)["x_hat"] with tempfile.TemporaryDirectory() as tmpdir: ckpt_path = Path(tmpdir) / "tactile_vae.pt" torch.save(model.state_dict(), ckpt_path) loaded = load_pretrained( checkpoint=str(ckpt_path), model_kwargs={ "img_size": 64, "patch_size": 16, "in_chans": 3, "embed_dim": 96, "encoder_depth": 2, "encoder_heads": 4, "decoder_embed_dim": 96, "decoder_depth": 2, "decoder_heads": 4, "latent_dim": 48, }, freeze=True, strict=True, ).eval() with torch.no_grad(): out_loaded = loaded(x, sample=False)["x_hat"] torch.testing.assert_close(out_ref, out_loaded, atol=1e-6, rtol=1e-5) if __name__ == "__main__": test_forward_contract_shapes() test_losses_finite_and_backprop() test_tiny_overfit_recon_drops() test_checkpoint_roundtrip_consistent_eval() print("All tactile VAE tests passed.")