Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import pickle | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from biolmnet.artifacts import load_bundle, save_bundle | |
| from biolmnet.data import ( | |
| BranchPriors, | |
| PreparedWorkspace, | |
| attach_embeddings_and_pathways, | |
| build_biological_mask, | |
| deterministic_gene_embeddings, | |
| ) | |
| from biolmnet.model import BioMaskedLinear, BioLMNet | |
| from biolmnet.training import Hyperparameters, predict, train | |
| def test_biological_mask_uses_pdi_and_undirected_ppi() -> None: | |
| pdi = pd.DataFrame( | |
| {"TF": ["A", "C", "outside"], "Target": ["B", "D", "A"]} | |
| ) | |
| ppi = pd.DataFrame( | |
| { | |
| "protein1": ["A", "X", "B"], | |
| "protein2": ["X", "C", "Y"], | |
| "combined_score": [950, 950, 710], | |
| } | |
| ) | |
| branch = build_biological_mask(["A", "B", "C", "D"], pdi, ppi) | |
| assert {"B", "D", "X"}.issubset(branch.hidden_genes) | |
| x_index = branch.hidden_genes.index("X") | |
| assert branch.biological_mask[0, x_index] > 0 | |
| assert branch.biological_mask[2, x_index] > 0 | |
| assert branch.pdi_edges == 2 | |
| def test_masked_linear_disconnects_unlisted_weights() -> None: | |
| mask = torch.tensor([[1.0, 0.0], [0.0, 1.0]]) | |
| layer = BioMaskedLinear(mask, bias=False) | |
| values = torch.tensor([[2.0, 3.0]]) | |
| baseline = layer(values).detach().clone() | |
| with torch.no_grad(): | |
| layer.weight[0, 1] = 10_000 | |
| layer.weight[1, 0] = -10_000 | |
| changed = layer(values).detach() | |
| assert torch.allclose(baseline, changed) | |
| def _tiny_workspace() -> PreparedWorkspace: | |
| rng = np.random.default_rng(7) | |
| sample_count = 48 | |
| labels = np.repeat(np.array([0, 1]), sample_count // 2) | |
| gene_values = rng.normal(size=(sample_count, 4)).astype(np.float32) | |
| dna_values = rng.normal(size=(sample_count, 4)).astype(np.float32) | |
| gene_values[:, 0] += labels * 1.5 | |
| dna_values[:, 1] -= labels * 1.2 | |
| input_genes = ["A", "B", "C", "D"] | |
| hidden_genes = ["A", "B", "C"] | |
| biological = np.array( | |
| [ | |
| [1.0, 1.0, 0.0], | |
| [0.0, 1.0, 1.0], | |
| [1.0, 0.0, 1.0], | |
| [0.0, 1.0, 0.0], | |
| ], | |
| dtype=np.float32, | |
| ) | |
| pathway_mapping = pd.DataFrame( | |
| { | |
| "SYMBOL": ["A", "B", "B", "C"], | |
| "PathwayID": ["hsa1", "hsa1", "hsa2", "hsa2"], | |
| } | |
| ) | |
| embeddings = deterministic_gene_embeddings(hidden_genes, dimensions=8) | |
| def branch() -> BranchPriors: | |
| value = BranchPriors( | |
| input_genes=input_genes.copy(), | |
| hidden_genes=hidden_genes.copy(), | |
| biological_mask=biological.copy(), | |
| pdi_edges=3, | |
| ppi_edges=4, | |
| ) | |
| attach_embeddings_and_pathways( | |
| value, | |
| embeddings, | |
| pathway_mapping, | |
| precomputed_significant=True, | |
| ) | |
| return value | |
| return PreparedWorkspace( | |
| gene_expression=gene_values, | |
| dna_methylation=dna_values, | |
| labels=labels, | |
| label_names=["control", "case"], | |
| gene_branch=branch(), | |
| dna_branch=branch(), | |
| source_name="unit test", | |
| ) | |
| def test_model_forward_probabilistic_shape() -> None: | |
| workspace = _tiny_workspace() | |
| gene = workspace.gene_branch | |
| dna = workspace.dna_branch | |
| model = BioLMNet( | |
| torch.from_numpy(gene.biological_mask), | |
| torch.from_numpy(dna.biological_mask), | |
| torch.from_numpy(gene.embeddings), | |
| torch.from_numpy(dna.embeddings), | |
| torch.from_numpy(gene.pathway_mask), | |
| torch.from_numpy(dna.pathway_mask), | |
| n_classes=2, | |
| projection_dim=4, | |
| fusion_dim=3, | |
| dropout=0.0, | |
| ) | |
| logits = model(torch.randn(5, 4), torch.randn(5, 4)) | |
| assert logits.shape == (5, 2) | |
| assert torch.allclose( | |
| model.gene_branch.pathway_attention.attention_weights().sum(dim=0), | |
| torch.ones(2), | |
| ) | |
| def test_training_artifact_roundtrip(tmp_path) -> None: | |
| workspace = _tiny_workspace() | |
| result = train( | |
| workspace, | |
| Hyperparameters( | |
| epochs=3, | |
| batch_size=8, | |
| projection_dim=4, | |
| fusion_dim=3, | |
| dropout=0.0, | |
| early_stopping_patience=3, | |
| ), | |
| ) | |
| restored_from_process_boundary = pickle.loads(pickle.dumps(result.bundle)) | |
| assert restored_from_process_boundary.label_names == ["control", "case"] | |
| artifact = save_bundle(result.bundle, tmp_path / "model.zip") | |
| restored = load_bundle(artifact) | |
| gene_frame = pd.DataFrame( | |
| workspace.gene_expression[:5], columns=restored.gene_features | |
| ) | |
| dna_frame = pd.DataFrame( | |
| workspace.dna_methylation[:5], columns=restored.dna_features | |
| ) | |
| before = predict(gene_frame, dna_frame, result.bundle) | |
| after = predict(gene_frame, dna_frame, restored) | |
| probability_columns = [column for column in before if column.startswith("P(")] | |
| np.testing.assert_allclose( | |
| before[probability_columns].to_numpy(), | |
| after[probability_columns].to_numpy(), | |
| atol=1e-6, | |
| ) | |
| def test_zerogpu_duration_estimator_is_bounded_and_scales() -> None: | |
| from app import estimate_training_duration | |
| workspace = _tiny_workspace() | |
| common = (workspace, 16, 0.001, 0.01, 0.3, 64, 12, 0.2, "Adam", True) | |
| short = estimate_training_duration(common[0], 10, *common[1:]) | |
| long = estimate_training_duration(common[0], 200, *common[1:]) | |
| assert 30 <= short <= 300 | |
| assert short <= long <= 300 | |