| """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") |
|
|
|
|
| |
| |
| |
| 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 |
|
|
| 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 |
|
|
|
|
| |
| |
| |
| @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() |
| |
| 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"]) |
|
|
|
|
| |
| |
| |
| @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() |
|
|