Download PDGrapher/tests/test_population_batch.py from Boom5426/GID-Flow: direct link, hf CLI and curl.
- Browser
- Download file 6.63 kB
-
https://huggingface.co/Boom5426/GID-Flow/resolve/main/PDGrapher/tests/test_population_batch.py
- Command line
-
hf download hf://Boom5426/GID-Flow/PDGrapher/tests/test_population_batch.py
-
curl -L -o test_population_batch.py https://huggingface.co/Boom5426/GID-Flow/resolve/main/PDGrapher/tests/test_population_batch.py
6.63 kB
| """Tests for population-level data layer: batch, dataset, collate.""" | |
| 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 ( | |
| PopulationPerturbationBatch, | |
| SyntheticPopulationPerturbationDataset, | |
| population_collate_fn, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # PopulationPerturbationBatch | |
| # --------------------------------------------------------------------------- | |
| class TestPopulationPerturbationBatch: | |
| def _make_batch(self, B=2, Ns=4, Nt=6, G=8): | |
| return PopulationPerturbationBatch( | |
| source_cells=torch.randn(B, Ns, G), | |
| target_cells=torch.randn(B, Nt, G), | |
| perturbation=torch.zeros(B, G), | |
| source_mask=torch.ones(B, Ns, dtype=torch.bool), | |
| target_mask=torch.ones(B, Nt, dtype=torch.bool), | |
| ) | |
| def test_shapes(self): | |
| b = self._make_batch() | |
| assert b.source_cells.shape == (2, 4, 8) | |
| assert b.target_cells.shape == (2, 6, 8) | |
| assert b.perturbation.shape == (2, 8) | |
| assert b.source_mask.shape == (2, 4) | |
| assert b.target_mask.shape == (2, 6) | |
| def test_to_device_cpu(self): | |
| b = self._make_batch() | |
| b2 = b.to(torch.device("cpu")) | |
| assert b2.source_cells.device.type == "cpu" | |
| def test_optional_fields_default_none(self): | |
| b = self._make_batch() | |
| assert b.context is None | |
| assert b.metadata is None | |
| # --------------------------------------------------------------------------- | |
| # SyntheticPopulationPerturbationDataset | |
| # --------------------------------------------------------------------------- | |
| class TestSyntheticPopulationPerturbationDataset: | |
| def ds(self): | |
| return SyntheticPopulationPerturbationDataset( | |
| num_conditions=16, | |
| num_genes=32, | |
| min_cells=4, | |
| max_cells=12, | |
| perturbation_size=2, | |
| seed=0, | |
| ) | |
| def test_length(self, ds): | |
| assert len(ds) == 16 | |
| def test_item_keys(self, ds): | |
| item = ds[0] | |
| assert "source_cells" in item | |
| assert "target_cells" in item | |
| assert "perturbation" in item | |
| def test_gene_dim(self, ds): | |
| item = ds[0] | |
| assert item["source_cells"].shape[-1] == 32 | |
| assert item["target_cells"].shape[-1] == 32 | |
| assert item["perturbation"].shape == (32,) | |
| def test_cell_counts_in_range(self, ds): | |
| for item in ds: | |
| ns = item["source_cells"].shape[0] | |
| nt = item["target_cells"].shape[0] | |
| assert 4 <= ns <= 12 | |
| assert 4 <= nt <= 12 | |
| def test_perturbation_is_multihot(self, ds): | |
| for item in ds: | |
| pert = item["perturbation"] | |
| assert ((pert == 0) | (pert == 1)).all() | |
| assert int(pert.sum().item()) == 2 | |
| def test_reproducible(self): | |
| ds1 = SyntheticPopulationPerturbationDataset(num_conditions=4, num_genes=8, seed=7) | |
| ds2 = SyntheticPopulationPerturbationDataset(num_conditions=4, num_genes=8, seed=7) | |
| assert torch.allclose(ds1[0]["source_cells"], ds2[0]["source_cells"]) | |
| def test_different_seeds_differ(self): | |
| ds1 = SyntheticPopulationPerturbationDataset(num_conditions=4, num_genes=16, seed=1) | |
| ds2 = SyntheticPopulationPerturbationDataset(num_conditions=4, num_genes=16, seed=2) | |
| # Perturbations are same size [G] regardless of cell count | |
| assert not torch.allclose(ds1[0]["perturbation"], ds2[0]["perturbation"]) | |
| # --------------------------------------------------------------------------- | |
| # population_collate_fn | |
| # --------------------------------------------------------------------------- | |
| class TestPopulationCollateFn: | |
| def _make_items(self, G=16): | |
| """Items with deliberately different cell counts.""" | |
| return [ | |
| {"source_cells": torch.randn(3, G), "target_cells": torch.randn(5, G), "perturbation": torch.zeros(G)}, | |
| {"source_cells": torch.randn(7, G), "target_cells": torch.randn(2, G), "perturbation": torch.ones(G)}, | |
| ] | |
| def test_output_type(self): | |
| items = self._make_items() | |
| batch = population_collate_fn(items) | |
| assert isinstance(batch, PopulationPerturbationBatch) | |
| def test_padded_shapes(self): | |
| items = self._make_items(G=16) | |
| batch = population_collate_fn(items) | |
| B, G = 2, 16 | |
| assert batch.source_cells.shape == (B, 7, G) # max(3, 7) | |
| assert batch.target_cells.shape == (B, 5, G) # max(5, 2) | |
| assert batch.perturbation.shape == (B, G) | |
| assert batch.source_mask.shape == (B, 7) | |
| assert batch.target_mask.shape == (B, 5) | |
| def test_mask_values(self): | |
| items = self._make_items(G=8) | |
| batch = population_collate_fn(items) | |
| # item 0 has 3 source cells → first 3 real, rest padding | |
| assert batch.source_mask[0, :3].all() | |
| assert not batch.source_mask[0, 3:].any() | |
| # item 1 has 7 source cells → all real | |
| assert batch.source_mask[1].all() | |
| def test_padding_is_zero(self): | |
| items = self._make_items(G=8) | |
| batch = population_collate_fn(items) | |
| # padded positions for item 0 (source cells 3..6) | |
| assert (batch.source_cells[0, 3:] == 0).all() | |
| def test_max_cells_truncation(self): | |
| items = self._make_items(G=8) | |
| batch = population_collate_fn(items, max_source_cells=4, max_target_cells=3) | |
| assert batch.source_cells.shape[1] == 4 | |
| assert batch.target_cells.shape[1] == 3 | |
| def test_dataloader_integration(self): | |
| ds = SyntheticPopulationPerturbationDataset(num_conditions=8, num_genes=16, seed=0) | |
| loader = DataLoader(ds, batch_size=4, collate_fn=population_collate_fn, shuffle=False) | |
| batch = next(iter(loader)) | |
| assert isinstance(batch, PopulationPerturbationBatch) | |
| assert batch.source_cells.shape[0] == 4 | |
| assert batch.source_cells.shape[2] == 16 | |
| def test_perturbation_stacked_correctly(self): | |
| G = 8 | |
| items = [ | |
| {"source_cells": torch.randn(2, G), "target_cells": torch.randn(2, G), | |
| "perturbation": torch.tensor([1.0, 0, 0, 0, 0, 0, 0, 0])}, | |
| {"source_cells": torch.randn(2, G), "target_cells": torch.randn(2, G), | |
| "perturbation": torch.tensor([0.0, 1, 0, 0, 0, 0, 0, 0])}, | |
| ] | |
| batch = population_collate_fn(items) | |
| assert batch.perturbation[0, 0] == 1.0 | |
| assert batch.perturbation[1, 1] == 1.0 | |