"""Unit tests for v9b modules: JEPA, DDPM decoder, SDF tower, two-tower combiner, JEPA conformal, mesh extraction, MNI152 registration. Run with: python tests/test_v9b_components.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import torch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from src.research.jepa import ( # noqa: E402 IJEPAModel, ViTEncoder, JEPAPredictor, make_jepa_masks, ) from src.research.latent_diffusion_decoder import LatentConditionedDDPM # noqa: E402 from src.research.sdf_geometric_tower import GeometricSDFTower # noqa: E402 from src.research.two_tower_anomaly import combine_two_towers, normalize_per_tower # noqa: E402 from src.research.jepa_conformal import JepaConformalCalibrator, weighted_quantile # noqa: E402 from src.research.mesh_extraction import extract_tumor_mesh, stack_2d_to_pseudo_3d # noqa: E402 from src.research.mni152_registration import ( # noqa: E402 voxel_to_mni_approx, distances_to_landmarks, tumor_atlas_report, ) from src.research.geometric_prior import synthetic_brain_sdf_template # noqa: E402 # ----------------- JEPA ----------------- def test_vit_encoder_shape(): enc = ViTEncoder(image_size=64, patch_size=8, embed_dim=64, depth=2, heads=4) x = torch.randn(2, 3, 64, 64) z = enc(x) assert z.shape == (2, 64, 64), f"got {z.shape}" # (B, N=8*8, D=64) def test_vit_encoder_keep_indices(): enc = ViTEncoder(image_size=64, patch_size=8, embed_dim=64, depth=2, heads=4) x = torch.randn(2, 3, 64, 64) keep = torch.arange(20).unsqueeze(0).expand(2, -1) z = enc(x, keep_indices=keep) assert z.shape == (2, 20, 64) def test_jepa_predictor_shape(): pred = JEPAPredictor(embed_dim=64, predictor_dim=32, depth=2, heads=4, grid_size=8) ctx = torch.randn(2, 20, 64) ci = torch.arange(20).unsqueeze(0).expand(2, -1) ti = torch.arange(20, 30).unsqueeze(0).expand(2, -1) out = pred(ctx, ci, ti) assert out.shape == (2, 10, 64) def test_make_jepa_masks(): masks = make_jepa_masks(grid_size=16, batch_size=4, n_target=2) assert masks["context_indices"].shape[0] == 4 assert masks["target_indices"].shape[0] == 4 assert masks["context_indices"].max() < 16 * 16 assert masks["target_indices"].max() < 16 * 16 def test_ijepa_training_forward(): model = IJEPAModel(image_size=64, patch_size=8, embed_dim=64, depth=2, heads=4, predictor_dim=32, predictor_depth=2) x = torch.randn(2, 3, 64, 64) masks = make_jepa_masks(model.grid_size, 2) out = model(x, masks) assert "loss" in out and out["loss"].requires_grad out["loss"].backward() def test_ijepa_ema_update(): model = IJEPAModel(image_size=64, patch_size=8, embed_dim=32, depth=2, heads=4, predictor_dim=16, predictor_depth=2) # Snapshot target weights p0 = next(model.target_encoder.parameters()).clone() # Modify context_encoder for p in model.context_encoder.parameters(): p.data.add_(torch.ones_like(p) * 0.1) model.ema_update() p1 = next(model.target_encoder.parameters()) # Target should have moved toward context (since momentum < 1) assert not torch.allclose(p0, p1), "EMA didn't update target weights" def test_ijepa_prediction_error_map(): model = IJEPAModel(image_size=32, patch_size=8, embed_dim=32, depth=2, heads=4, predictor_dim=16, predictor_depth=2) x = torch.randn(1, 3, 32, 32) emap = model.prediction_error_map(x) assert emap.shape == (1, 1, 32, 32) assert (emap >= 0).all() # ----------------- DDPM ----------------- def test_ddpm_training_loss(): ddpm = LatentConditionedDDPM(in_chans=3, base_ch=8, cond_dim=16, num_train_timesteps=100) x = torch.randn(2, 3, 32, 32) cond = torch.randn(2, 16) loss = ddpm.training_loss(x, cond) assert loss.requires_grad loss.backward() def test_ddpm_ddim_sampling(): ddpm = LatentConditionedDDPM(in_chans=3, base_ch=8, cond_dim=16, num_train_timesteps=100) cond = torch.randn(1, 16) out = ddpm.ddim_sample((1, 3, 32, 32), cond, num_steps=5, device="cpu") assert out.shape == (1, 3, 32, 32) # ----------------- SDF tower ----------------- def test_sdf_tower_forward(): tower = GeometricSDFTower(image_size=64, base_ch=8) x = torch.randn(2, 3, 64, 64) sdf = tower(x) assert sdf.shape == (2, 1, 64, 64) def test_sdf_tower_training_loss(): tower = GeometricSDFTower(image_size=64, base_ch=8) x = torch.randn(2, 3, 64, 64) tpl = synthetic_brain_sdf_template(64).unsqueeze(0).unsqueeze(0).expand(2, -1, -1, -1) loss = tower.training_loss(x, tpl) assert loss.requires_grad # ----------------- Two-tower combiner ----------------- def test_combine_two_towers_weighted_sum(): a = torch.rand(2, 1, 32, 32) g = torch.rand(2, 1, 32, 32) c = combine_two_towers(a, g, mode="weighted_sum", lambda_app=0.5, lambda_geo=0.5) assert c.shape == (2, 1, 32, 32) assert (c >= 0).all() and (c <= 1).all() def test_combine_two_towers_and(): a = torch.rand(2, 1, 32, 32) g = torch.rand(2, 1, 32, 32) c = combine_two_towers(a, g, mode="and", q_app=0.5, q_geo=0.5) assert c.shape == (2, 1, 32, 32) assert set(c.unique().tolist()).issubset({0.0, 1.0}) # ----------------- JEPA conformal ----------------- def test_weighted_quantile_uniform(): v = np.linspace(0, 1, 100) w = np.ones(100) q = weighted_quantile(v, w, 0.9) assert 0.85 < q < 0.95 def test_jepa_conformal_calibrator(): np.random.seed(0) scores = np.random.exponential(1.0, 200).tolist() cal = JepaConformalCalibrator(alpha=0.10) report = cal.calibrate(scores, verbose=False) assert report.empirical_coverage >= 0.85 assert cal.q > 0 def test_jepa_conformal_predict_voxelwise(): cal = JepaConformalCalibrator(alpha=0.10) cal.calibrate(np.random.exponential(1.0, 100).tolist()) errors = torch.rand(2, 1, 16, 16) mask = cal.predict_voxelwise_certified(errors) assert mask.shape == (2, 1, 16, 16) assert mask.dtype == torch.bool # ----------------- Mesh extraction ----------------- def test_extract_tumor_mesh_simple(): try: import skimage # noqa except ImportError: print("scikit-image not installed, skipping mesh test") return vol = np.zeros((10, 32, 32), dtype=np.float32) vol[3:7, 12:20, 12:20] = 1.0 mesh = extract_tumor_mesh(vol) assert mesh["n_verts"] > 0 assert mesh["n_faces"] > 0 assert mesh["volume_mm3"] == 4 * 8 * 8 # cube voxels def test_stack_2d_to_pseudo_3d(): m = np.ones((32, 32)) vol = stack_2d_to_pseudo_3d(m) assert vol.shape == (1, 32, 32) # ----------------- MNI152 registration ----------------- def test_voxel_to_mni_center(): mni = voxel_to_mni_approx((0, 128, 128), (1, 256, 256), input_orientation="axial_2d") assert -1 < mni[0] < 1 # center x assert -20 < mni[1] < 20 # center y near y=0 assert mni[2] == 0.0 # 2D -> z=0 def test_distances_to_landmarks(): d = distances_to_landmarks((0, 0, 0)) for v in d.values(): assert v >= 0 def test_tumor_atlas_report_full(): vol = np.zeros((1, 256, 256), dtype=np.float32) vol[0, 100:150, 100:150] = 1.0 rep = tumor_atlas_report(vol) assert rep["volume_mm3"] == 50 * 50 assert rep["centroid_mni"] is not None assert len(rep["nearest_landmarks"]) == 3 if __name__ == "__main__": print("=== JEPA ===") test_vit_encoder_shape(); test_vit_encoder_keep_indices() test_jepa_predictor_shape(); test_make_jepa_masks() test_ijepa_training_forward(); test_ijepa_ema_update() test_ijepa_prediction_error_map() print("=== DDPM decoder ===") test_ddpm_training_loss(); test_ddpm_ddim_sampling() print("=== SDF tower ===") test_sdf_tower_forward(); test_sdf_tower_training_loss() print("=== Two-tower combiner ===") test_combine_two_towers_weighted_sum(); test_combine_two_towers_and() print("=== JEPA conformal ===") test_weighted_quantile_uniform(); test_jepa_conformal_calibrator() test_jepa_conformal_predict_voxelwise() print("=== Mesh extraction ===") test_extract_tumor_mesh_simple(); test_stack_2d_to_pseudo_3d() print("=== MNI152 registration ===") test_voxel_to_mni_center(); test_distances_to_landmarks() test_tumor_atlas_report_full() print("\nAll v9b component tests passed.")