scPTR / tests /test_deep_model.py
bryan7264's picture
Add files using upload-large-folder tool
9d901ad verified
Raw
History Blame Contribute Delete
6.35 kB
"""Tests for DeepPTR model components."""
import numpy as np
import pytest
import torch
from scptr.deep._model import DeepPTR, Encoder, KineticDecoder
@pytest.fixture
def model_dims():
return {"n_genes": 50, "d_T": 5, "d_PT": 5, "d_hidden": 32}
@pytest.fixture
def batch_data(model_dims):
"""Synthetic batch of data."""
torch.manual_seed(0)
n = 16
G = model_dims["n_genes"]
return {
"s": torch.rand(n, G) * 10,
"u": torch.rand(n, G) * 5,
"l_s": torch.rand(n) * 1000 + 100,
"l_u": torch.rand(n) * 500 + 50,
}
class TestEncoder:
def test_output_shapes(self, model_dims):
enc = Encoder(
n_genes=model_dims["n_genes"],
d_hidden=model_dims["d_hidden"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
)
s = torch.rand(8, model_dims["n_genes"])
u = torch.rand(8, model_dims["n_genes"])
mu_T, logvar_T, mu_PT, logvar_PT = enc(s, u)
assert mu_T.shape == (8, model_dims["d_T"])
assert logvar_T.shape == (8, model_dims["d_T"])
assert mu_PT.shape == (8, model_dims["d_PT"])
assert logvar_PT.shape == (8, model_dims["d_PT"])
def test_different_inputs_different_outputs(self, model_dims):
enc = Encoder(
n_genes=model_dims["n_genes"],
d_hidden=model_dims["d_hidden"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
)
s1 = torch.rand(1, model_dims["n_genes"])
u1 = torch.rand(1, model_dims["n_genes"])
s2 = torch.rand(1, model_dims["n_genes"]) + 5
u2 = torch.rand(1, model_dims["n_genes"]) + 5
out1 = enc(s1, u1)
out2 = enc(s2, u2)
assert not torch.allclose(out1[0], out2[0])
class TestKineticDecoder:
def test_output_shapes(self, model_dims):
dec = KineticDecoder(
n_genes=model_dims["n_genes"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
d_hidden=model_dims["d_hidden"],
)
z_T = torch.randn(8, model_dims["d_T"])
z_PT = torch.randn(8, model_dims["d_PT"])
l_s = torch.ones(8) * 1000
l_u = torch.ones(8) * 500
out = dec(z_T, z_PT, l_s, l_u)
G = model_dims["n_genes"]
assert out["mu_s"].shape == (8, G)
assert out["mu_u"].shape == (8, G)
assert out["alpha"].shape == (8, G)
assert out["gamma"].shape == (8, G)
assert out["beta"].shape == (G,)
assert out["theta_s"].shape == (G,)
assert out["theta_u"].shape == (G,)
def test_positive_outputs(self, model_dims):
dec = KineticDecoder(
n_genes=model_dims["n_genes"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
d_hidden=model_dims["d_hidden"],
)
z_T = torch.randn(16, model_dims["d_T"])
z_PT = torch.randn(16, model_dims["d_PT"])
l_s = torch.ones(16) * 1000
l_u = torch.ones(16) * 500
out = dec(z_T, z_PT, l_s, l_u)
for key in ("mu_s", "mu_u", "alpha", "gamma", "beta", "theta_s", "theta_u"):
assert (out[key] >= 0).all(), f"{key} has negative values"
def test_mu_scales_with_library_size(self, model_dims):
dec = KineticDecoder(
n_genes=model_dims["n_genes"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
d_hidden=model_dims["d_hidden"],
)
z_T = torch.randn(1, model_dims["d_T"])
z_PT = torch.randn(1, model_dims["d_PT"])
out1 = dec(z_T, z_PT, torch.tensor([100.0]), torch.tensor([100.0]))
out2 = dec(z_T, z_PT, torch.tensor([1000.0]), torch.tensor([1000.0]))
ratio_s = out2["mu_s"].sum() / out1["mu_s"].sum()
assert abs(ratio_s.item() - 10.0) < 1.0
def test_beta_is_not_cell_specific(self, model_dims):
"""Beta should be the same regardless of input."""
dec = KineticDecoder(
n_genes=model_dims["n_genes"],
d_T=model_dims["d_T"],
d_PT=model_dims["d_PT"],
d_hidden=model_dims["d_hidden"],
)
z_T1 = torch.randn(4, model_dims["d_T"])
z_PT1 = torch.randn(4, model_dims["d_PT"])
z_T2 = torch.randn(4, model_dims["d_T"])
z_PT2 = torch.randn(4, model_dims["d_PT"])
out1 = dec(z_T1, z_PT1, torch.ones(4), torch.ones(4))
out2 = dec(z_T2, z_PT2, torch.ones(4), torch.ones(4))
assert torch.allclose(out1["beta"], out2["beta"])
class TestDeepPTR:
def test_forward_loss(self, model_dims, batch_data):
model = DeepPTR(**model_dims)
out = model(**batch_data)
assert "loss" in out
assert "recon_loss" in out
assert "kl_loss" in out
assert not torch.isnan(out["loss"])
def test_backward(self, model_dims, batch_data):
model = DeepPTR(**model_dims)
out = model(**batch_data)
out["loss"].backward()
for name, p in model.named_parameters():
if p.requires_grad:
assert p.grad is not None, f"No gradient for {name}"
assert not torch.isnan(p.grad).any(), f"NaN gradient for {name}"
def test_kl_weight_zero(self, model_dims, batch_data):
model = DeepPTR(**model_dims)
out_0 = model(**batch_data, kl_weight=0.0)
out_1 = model(**batch_data, kl_weight=1.0)
# With kl_weight=0, loss should equal recon_loss
assert torch.allclose(out_0["loss"], out_0["recon_loss"], atol=1e-5)
# With kl_weight=1, loss > recon_loss (KL >= 0)
assert out_1["loss"] >= out_1["recon_loss"] - 1e-5
def test_get_latent(self, model_dims, batch_data):
model = DeepPTR(**model_dims)
mu_T, logvar_T, mu_PT, logvar_PT = model.get_latent(
batch_data["s"], batch_data["u"]
)
assert mu_T.shape == (16, model_dims["d_T"])
assert mu_PT.shape == (16, model_dims["d_PT"])
def test_reparameterize_stochastic(self, model_dims):
mu = torch.zeros(10, model_dims["d_T"])
logvar = torch.zeros(10, model_dims["d_T"])
z1 = DeepPTR.reparameterize(mu, logvar)
z2 = DeepPTR.reparameterize(mu, logvar)
# Two samples should differ (with overwhelming probability)
assert not torch.allclose(z1, z2)