Spaces:
Running on Zero
Running on Zero
| """Tests for reproducibility of InstaNovo predictions. | |
| These tests verify that predictions are consistent between runs, addressing | |
| the variability issue reported in: https://github.com/instadeepai/InstaNovo/issues/92 | |
| The key issue was that running InstaNovo locally vs on HuggingFace Spaces | |
| produced different results. These tests ensure: | |
| 1. Predictions are deterministic with fixed seeds | |
| 2. Multiple runs produce identical results | |
| 3. The sample spectra produce consistent outputs | |
| """ | |
| import pytest | |
| import os | |
| import sys | |
| import json | |
| from pathlib import Path | |
| # Add parent directory to path for imports | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| def load_app_components(): | |
| """Load the app components once for all tests.""" | |
| # Import as a module so we read live globals (populated by load_models_and_knapsack) | |
| # rather than capturing None values that existed at import time. | |
| import app | |
| app.set_seed(42) | |
| app.load_models_and_knapsack() | |
| return { | |
| "set_seed": app.set_seed, | |
| "create_inference_config": app.create_inference_config, | |
| "run_transformer_prediction": app.run_transformer_prediction, | |
| "run_diffusion_prediction": app.run_diffusion_prediction, | |
| "INSTANOVO": app.INSTANOVO, | |
| "INSTANOVOPLUS": app.INSTANOVOPLUS, | |
| "RESIDUE_SET": app.RESIDUE_SET, | |
| "DEVICE": app.DEVICE, | |
| } | |
| def sample_spectra_path(): | |
| """Path to sample spectra file.""" | |
| return Path(__file__).parent.parent / "assets" / "sample_spectra.mgf" | |
| def expected_results_path(): | |
| """Path to expected results file for comparison.""" | |
| return Path(__file__).parent / "expected_results.json" | |
| class TestSeedSetting: | |
| """Test that random seed setting works correctly.""" | |
| def test_seed_produces_consistent_random_numbers(self): | |
| """Verify that setting seed produces the same random sequence.""" | |
| import random | |
| import numpy as np | |
| import torch | |
| from app import set_seed | |
| # First run | |
| set_seed(42) | |
| rand1 = random.random() | |
| np1 = np.random.random() | |
| torch1 = torch.rand(1).item() | |
| # Second run with same seed | |
| set_seed(42) | |
| rand2 = random.random() | |
| np2 = np.random.random() | |
| torch2 = torch.rand(1).item() | |
| assert rand1 == rand2, "Python random not reproducible" | |
| assert np1 == np2, "NumPy random not reproducible" | |
| assert torch1 == torch2, "PyTorch random not reproducible" | |
| def test_different_seeds_produce_different_results(self): | |
| """Verify that different seeds produce different results.""" | |
| import random | |
| import numpy as np | |
| import torch | |
| from app import set_seed | |
| set_seed(42) | |
| results_42 = (random.random(), np.random.random(), torch.rand(1).item()) | |
| set_seed(123) | |
| results_123 = (random.random(), np.random.random(), torch.rand(1).item()) | |
| assert results_42 != results_123, "Different seeds should produce different results" | |
| class TestPredictionReproducibility: | |
| """Test that predictions are reproducible.""" | |
| def test_transformer_prediction_reproducibility( | |
| self, load_app_components, sample_spectra_path, tmp_path | |
| ): | |
| """Test that transformer predictions are reproducible across multiple runs.""" | |
| if not sample_spectra_path.exists(): | |
| pytest.skip(f"Sample spectra not found at {sample_spectra_path}") | |
| components = load_app_components | |
| if components["INSTANOVO"] is None: | |
| pytest.skip("InstaNovo model not loaded") | |
| from instanovo.utils.data_handler import SpectrumDataFrame | |
| from instanovo.transformer.data import TransformerDataProcessor | |
| from torch.utils.data import DataLoader | |
| set_seed = components["set_seed"] | |
| create_inference_config = components["create_inference_config"] | |
| run_transformer_prediction = components["run_transformer_prediction"] | |
| # Create config | |
| output_path = tmp_path / "output.csv" | |
| config = create_inference_config(str(sample_spectra_path), str(output_path)) | |
| # Load data | |
| sdf = SpectrumDataFrame.load( | |
| str(sample_spectra_path), | |
| lazy=False, | |
| is_annotated=False, | |
| shuffle=False, | |
| ) | |
| import app | |
| RESIDUE_SET = app.RESIDUE_SET | |
| INSTANOVO_CONFIG = app.INSTANOVO_CONFIG | |
| data_processor = TransformerDataProcessor( | |
| residue_set=RESIDUE_SET, | |
| n_peaks=INSTANOVO_CONFIG.get("n_peaks", 200), | |
| annotated=False, | |
| reverse_peptide=True, | |
| add_eos=True, | |
| ) | |
| dataset = sdf.to_dataset() | |
| def _make_dl(): | |
| processed = data_processor.process_dataset(dataset) | |
| return DataLoader( | |
| processed, | |
| batch_size=config.batch_size, | |
| shuffle=False, | |
| collate_fn=data_processor.collate_fn, | |
| ) | |
| # First run | |
| set_seed(42) | |
| results1 = run_transformer_prediction(_make_dl(), config, "Greedy Search (Fast)") | |
| predictions1 = ["".join(r.sequence) if r.sequence else "" for r in results1] | |
| log_probs1 = [r.sequence_log_probability for r in results1] | |
| # Second run | |
| set_seed(42) | |
| results2 = run_transformer_prediction(_make_dl(), config, "Greedy Search (Fast)") | |
| predictions2 = ["".join(r.sequence) if r.sequence else "" for r in results2] | |
| log_probs2 = [r.sequence_log_probability for r in results2] | |
| # Verify identical results | |
| assert predictions1 == predictions2, ( | |
| f"Predictions differ between runs:\n" | |
| f"Run 1: {predictions1[:5]}\n" | |
| f"Run 2: {predictions2[:5]}" | |
| ) | |
| assert log_probs1 == log_probs2, "Log probabilities differ between runs" | |
| class TestExpectedResults: | |
| """Test that predictions match expected baseline results. | |
| These tests compare against known-good results to catch any regressions | |
| or environment-specific differences. | |
| """ | |
| def test_predictions_match_expected_baseline( | |
| self, load_app_components, sample_spectra_path, expected_results_path, tmp_path | |
| ): | |
| """Test that predictions match the expected baseline results.""" | |
| if not sample_spectra_path.exists(): | |
| pytest.skip(f"Sample spectra not found at {sample_spectra_path}") | |
| if not expected_results_path.exists(): | |
| pytest.skip( | |
| f"Expected results file not found at {expected_results_path}. " | |
| "Run 'pytest tests/test_reproducibility.py::TestExpectedResults::test_generate_expected_results' first." | |
| ) | |
| components = load_app_components | |
| if components["INSTANOVO"] is None: | |
| pytest.skip("InstaNovo model not loaded") | |
| from instanovo.utils.data_handler import SpectrumDataFrame | |
| from instanovo.transformer.data import TransformerDataProcessor | |
| from torch.utils.data import DataLoader | |
| set_seed = components["set_seed"] | |
| create_inference_config = components["create_inference_config"] | |
| run_transformer_prediction = components["run_transformer_prediction"] | |
| # Load expected results | |
| with open(expected_results_path) as f: | |
| expected = json.load(f) | |
| # Create config | |
| output_path = tmp_path / "output.csv" | |
| config = create_inference_config(str(sample_spectra_path), str(output_path)) | |
| # Load data | |
| sdf = SpectrumDataFrame.load( | |
| str(sample_spectra_path), | |
| lazy=False, | |
| is_annotated=False, | |
| shuffle=False | |
| ) | |
| import app | |
| RESIDUE_SET = app.RESIDUE_SET | |
| INSTANOVO_CONFIG = app.INSTANOVO_CONFIG | |
| # Use new v1.2.2 API | |
| data_processor = TransformerDataProcessor( | |
| residue_set=RESIDUE_SET, | |
| n_peaks=INSTANOVO_CONFIG.get("n_peaks", 200), | |
| annotated=False, | |
| reverse_peptide=True, | |
| add_eos=True, | |
| ) | |
| dataset = sdf.to_dataset() | |
| processed_dataset = data_processor.process_dataset(dataset) | |
| dl = DataLoader( | |
| processed_dataset, | |
| batch_size=config.batch_size, | |
| shuffle=False, | |
| collate_fn=data_processor.collate_fn | |
| ) | |
| # Run prediction | |
| set_seed(42) | |
| results = run_transformer_prediction(dl, config, "Greedy Search (Fast)") | |
| predictions = ["".join(r.sequence) if r.sequence else "" for r in results] | |
| # Compare against expected | |
| expected_predictions = expected.get("greedy_predictions", []) | |
| assert predictions == expected_predictions, ( | |
| f"Predictions differ from expected baseline:\n" | |
| f"Got: {predictions[:5]}\n" | |
| f"Expected: {expected_predictions[:5]}\n\n" | |
| "This may indicate a reproducibility issue. " | |
| "If the new results are correct, regenerate expected_results.json" | |
| ) | |
| def test_generate_expected_results( | |
| self, load_app_components, sample_spectra_path, tmp_path | |
| ): | |
| """Generate expected results file for baseline comparison. | |
| This is not a real test - it's a utility to generate the expected results. | |
| Run with: pytest tests/test_reproducibility.py::TestExpectedResults::test_generate_expected_results -s | |
| """ | |
| if not sample_spectra_path.exists(): | |
| pytest.skip(f"Sample spectra not found at {sample_spectra_path}") | |
| components = load_app_components | |
| if components["INSTANOVO"] is None: | |
| pytest.skip("InstaNovo model not loaded") | |
| from instanovo.utils.data_handler import SpectrumDataFrame | |
| from instanovo.transformer.data import TransformerDataProcessor | |
| from torch.utils.data import DataLoader | |
| set_seed = components["set_seed"] | |
| create_inference_config = components["create_inference_config"] | |
| run_transformer_prediction = components["run_transformer_prediction"] | |
| # Create config | |
| output_path = tmp_path / "output.csv" | |
| config = create_inference_config(str(sample_spectra_path), str(output_path)) | |
| # Load data | |
| sdf = SpectrumDataFrame.load( | |
| str(sample_spectra_path), | |
| lazy=False, | |
| is_annotated=False, | |
| shuffle=False | |
| ) | |
| import app | |
| RESIDUE_SET = app.RESIDUE_SET | |
| INSTANOVO_CONFIG = app.INSTANOVO_CONFIG | |
| # Use new v1.2.2 API | |
| data_processor = TransformerDataProcessor( | |
| residue_set=RESIDUE_SET, | |
| n_peaks=INSTANOVO_CONFIG.get("n_peaks", 200), | |
| annotated=False, | |
| reverse_peptide=True, | |
| add_eos=True, | |
| ) | |
| dataset = sdf.to_dataset() | |
| processed_dataset = data_processor.process_dataset(dataset) | |
| dl = DataLoader( | |
| processed_dataset, | |
| batch_size=config.batch_size, | |
| shuffle=False, | |
| collate_fn=data_processor.collate_fn | |
| ) | |
| # Run prediction with greedy | |
| set_seed(42) | |
| results = run_transformer_prediction(dl, config, "Greedy Search (Fast)") | |
| greedy_predictions = ["".join(r.sequence) if r.sequence else "" for r in results] | |
| greedy_log_probs = [r.sequence_log_probability for r in results] | |
| # Save expected results | |
| expected_results_path = Path(__file__).parent / "expected_results.json" | |
| expected = { | |
| "version": "1.2.2", | |
| "model": "instanovo-v1.2.0", | |
| "seed": 42, | |
| "greedy_predictions": greedy_predictions, | |
| "greedy_log_probs": greedy_log_probs, | |
| } | |
| with open(expected_results_path, "w") as f: | |
| json.dump(expected, f, indent=2) | |
| print(f"\nExpected results saved to {expected_results_path}") | |
| print(f"Predictions: {greedy_predictions}") | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |