GID-Flow / PDGrapher /tests /test_real_data.py
Boom5426's picture
Upload GID-Flow project snapshot (deduped: code + key artifacts)
07fcdfe verified
Raw
History Blame Contribute Delete
6.03 kB
"""Tests for real data interfaces (scPerturb + PDGrapher).
Norman2019 and PDGrapher real_lognorm are loaded from disk; tests skip
gracefully when the files are not present.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import pytest
import torch
from torch.utils.data import DataLoader
from gidflow.data import (
ScPerturbPopulationDataset,
PDGrapherPseudobulkDataset,
population_collate_fn,
select_hvg_by_variance,
normalize_counts,
)
import numpy as np
NORMAN_PATH = (
"/data/boom/Protein/regulatory_field/data/raw/"
"scPerturb/rna_protein/NormanWeissman2019_filtered.h5ad"
)
PDG_DIR = "/data/boom/Protein/PDGrapher/data/processed/torch_data/real_lognorm"
norman_available = pytest.mark.skipif(not os.path.exists(NORMAN_PATH), reason="Norman2019 h5ad not found")
pdgrapher_available = pytest.mark.skipif(not os.path.isdir(PDG_DIR), reason="PDGrapher real_lognorm not found")
# ---------------------------------------------------------------------------
# gene_selection utils
# ---------------------------------------------------------------------------
class TestGeneSelection:
def test_hvg_shape(self):
X = np.random.randn(100, 500).astype(np.float32)
idx = select_hvg_by_variance(X, n_genes=50)
assert idx.shape == (50,)
assert len(set(idx)) == 50
def test_hvg_fewer_than_requested(self):
X = np.random.randn(10, 20).astype(np.float32)
idx = select_hvg_by_variance(X, n_genes=100)
assert len(idx) == 20 # capped at n_genes
def test_normalize_no_log(self):
X = np.array([[1.0, 3.0], [2.0, 2.0]], dtype=np.float32)
out = normalize_counts(X, target_sum=4.0, log1p=False)
assert np.allclose(out.sum(axis=1), [4.0, 4.0])
def test_normalize_log1p(self):
X = np.ones((5, 10), dtype=np.float32) * 100
out = normalize_counts(X, target_sum=1e4, log1p=True)
assert (out > 0).all()
assert out.dtype == np.float32
# ---------------------------------------------------------------------------
# ScPerturbPopulationDataset (Norman2019)
# ---------------------------------------------------------------------------
@norman_available
class TestScPerturbPopulationDataset:
@pytest.fixture(scope="class")
def ds(self):
import warnings; warnings.filterwarnings("ignore")
return ScPerturbPopulationDataset(
NORMAN_PATH,
n_hvg=500,
min_cells_per_cond=30,
max_source_cells=16,
max_target_cells=16,
use_single_pert_only=True,
force_include_pert_genes=True,
seed=0,
)
def test_num_conditions(self, ds):
assert len(ds) >= 50, f"Expected ≥50 conditions, got {len(ds)}"
def test_item_shapes(self, ds):
item = ds[0]
G = ds.num_genes
assert item["source_cells"].shape == (16, G)
assert item["target_cells"].shape == (16, G)
assert item["perturbation"].shape == (G,)
def test_perturbation_is_multihot(self, ds):
item = ds[0]
pert = item["perturbation"]
assert ((pert == 0) | (pert == 1)).all()
assert pert.sum() >= 1
def test_most_perts_in_gene_space(self, ds):
covered = ds.n_genes_in_hvg_that_are_targets
assert covered / len(ds) >= 0.8, f"Only {covered}/{len(ds)} pert genes covered"
def test_source_target_differ(self, ds):
item = ds[5]
src_mean = item["source_cells"].mean()
tgt_mean = item["target_cells"].mean()
# They come from different cell populations — means will differ slightly
assert item["source_cells"].shape == item["target_cells"].shape
def test_collate_fn_integration(self, ds):
loader = DataLoader(ds, batch_size=4, collate_fn=population_collate_fn, shuffle=False)
batch = next(iter(loader))
assert batch.source_cells.shape[0] == 4
assert batch.source_cells.shape[2] == ds.num_genes
assert not batch.source_cells.isnan().any()
def test_reproducibility(self):
import warnings; warnings.filterwarnings("ignore")
ds1 = ScPerturbPopulationDataset(NORMAN_PATH, n_hvg=200, min_cells_per_cond=30,
max_source_cells=8, seed=7)
ds2 = ScPerturbPopulationDataset(NORMAN_PATH, n_hvg=200, min_cells_per_cond=30,
max_source_cells=8, seed=7)
item1 = ds1[0]
item2 = ds2[0]
assert torch.allclose(item1["perturbation"], item2["perturbation"])
# ---------------------------------------------------------------------------
# PDGrapherPseudobulkDataset
# ---------------------------------------------------------------------------
@pdgrapher_available
class TestPDGrapherPseudobulkDataset:
@pytest.fixture(scope="class")
def ds(self):
return PDGrapherPseudobulkDataset(PDG_DIR, cell_lines=["A549"], max_items=200)
def test_length(self, ds):
assert len(ds) == 200
def test_item_shapes(self, ds):
item = ds[0]
G = ds.num_genes
assert item["source_cells"].shape == (1, G)
assert item["target_cells"].shape == (1, G)
assert item["perturbation"].shape == (G,)
def test_num_genes(self, ds):
assert ds.num_genes == 10716
def test_perturbation_nonzero(self, ds):
for i in range(10):
item = ds[i]
assert item["perturbation"].sum() >= 1
def test_collate_fn_integration(self, ds):
loader = DataLoader(ds, batch_size=4, collate_fn=population_collate_fn)
batch = next(iter(loader))
assert batch.source_cells.shape == (4, 1, ds.num_genes)
assert batch.target_cells.shape == (4, 1, ds.num_genes)
def test_no_nan(self, ds):
for i in range(5):
item = ds[i]
assert not item["source_cells"].isnan().any()
assert not item["target_cells"].isnan().any()