Skip to content

Commit 7e29c27

Browse files
committed
Cleanup: add pytest configuration and shared fixtures for model tests
1 parent 9377a07 commit 7e29c27

10 files changed

Lines changed: 4072 additions & 0 deletions

tests/conftest.py

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
"""
2+
Pytest configuration and shared fixtures for model tests.
3+
4+
This module provides common fixtures and utilities for testing the UQDD models,
5+
following patterns similar to test_data_papyrus.py.
6+
"""
7+
8+
import pytest
9+
import torch
10+
import torch.nn as nn
11+
import numpy as np
12+
from pathlib import Path
13+
from unittest.mock import MagicMock, patch
14+
from typing import Dict, Tuple, Optional
15+
16+
import uqdd.models.utils_models as um
17+
from uqdd.models.pnn import PNN
18+
19+
20+
# ============================================================================
21+
# DEVICE AND DTYPE FIXTURES
22+
# ============================================================================
23+
24+
@pytest.fixture
25+
def device():
26+
"""Return CPU device for testing (avoid GPU issues)."""
27+
return torch.device("cpu")
28+
29+
30+
@pytest.fixture
31+
def dtype():
32+
"""Return default float dtype for tensors."""
33+
return torch.float32
34+
35+
36+
# ============================================================================
37+
# TENSOR AND BATCH FIXTURES
38+
# ============================================================================
39+
40+
@pytest.fixture
41+
def sample_tensor_2d(device, dtype):
42+
"""Create a sample 2D tensor (batch_size=8, features=10)."""
43+
return torch.randn(8, 10, device=device, dtype=dtype)
44+
45+
46+
@pytest.fixture
47+
def sample_tensor_3d(device, dtype):
48+
"""Create a sample 3D tensor (batch_size=4, features=5, time_steps=3)."""
49+
return torch.randn(4, 5, 3, device=device, dtype=dtype)
50+
51+
52+
@pytest.fixture
53+
def batch_proteins(device, dtype):
54+
"""Create a batch of protein descriptors (batch_size=8, prot_dim=256)."""
55+
return torch.randn(8, 256, device=device, dtype=dtype)
56+
57+
58+
@pytest.fixture
59+
def batch_chemicals(device, dtype):
60+
"""Create a batch of chemical descriptors (batch_size=8, chem_dim=2048)."""
61+
return torch.randn(8, 2048, device=device, dtype=dtype)
62+
63+
64+
@pytest.fixture
65+
def batch_targets(device, dtype):
66+
"""Create a batch of regression targets (batch_size=8)."""
67+
return torch.randn(8, 1, device=device, dtype=dtype)
68+
69+
70+
@pytest.fixture
71+
def batch_labels_binary(device):
72+
"""Create a batch of binary classification labels (batch_size=8)."""
73+
return torch.randint(0, 2, (8, 1), device=device, dtype=torch.float32)
74+
75+
76+
# ============================================================================
77+
# MODEL CONFIGURATION FIXTURES
78+
# ============================================================================
79+
80+
@pytest.fixture
81+
def minimal_pnn_config():
82+
"""Minimal PNN configuration for testing."""
83+
return {
84+
"chem_input_dim": 2048,
85+
"prot_input_dim": 256,
86+
"chem_hidden_dims": [512, 256],
87+
"prot_hidden_dims": [256, 128],
88+
"hidden_dims": [256, 128],
89+
"output_dim": 1,
90+
"dropout": 0.2,
91+
"task_type": "regression",
92+
"aleatoric": False,
93+
"n_targets": -1,
94+
"MT": False,
95+
}
96+
97+
98+
@pytest.fixture
99+
def minimal_ensemble_config():
100+
"""Minimal ensemble configuration for testing."""
101+
return {
102+
"chem_input_dim": 2048,
103+
"prot_input_dim": 256,
104+
"chem_hidden_dims": [512, 256],
105+
"prot_hidden_dims": [256, 128],
106+
"hidden_dims": [256, 128],
107+
"output_dim": 1,
108+
"dropout": 0.2,
109+
"task_type": "regression",
110+
"aleatoric": False,
111+
"ensemble_size": 3,
112+
"seed": 42,
113+
"n_targets": -1,
114+
"MT": False,
115+
}
116+
117+
118+
@pytest.fixture
119+
def minimal_evidential_config():
120+
"""Minimal evidential model configuration for testing."""
121+
return {
122+
"chem_input_dim": 2048,
123+
"prot_input_dim": 256,
124+
"chem_hidden_dims": [512, 256],
125+
"prot_hidden_dims": [256, 128],
126+
"hidden_dims": [256, 128],
127+
"output_dim": 1,
128+
"dropout": 0.2,
129+
"task_type": "regression",
130+
"aleatoric": False,
131+
"n_targets": -1,
132+
"MT": False,
133+
}
134+
135+
136+
# ============================================================================
137+
# MODEL FIXTURES
138+
# ============================================================================
139+
140+
@pytest.fixture
141+
def pnn_model(minimal_pnn_config, device):
142+
"""Create a simple PNN model for testing."""
143+
model = PNN(config=minimal_pnn_config)
144+
model.to(device)
145+
model.eval()
146+
return model
147+
148+
149+
@pytest.fixture
150+
def pnn_model_with_aleatoric(minimal_pnn_config, device):
151+
"""Create a PNN model with aleatoric uncertainty for testing."""
152+
config = minimal_pnn_config.copy()
153+
config["aleatoric"] = True
154+
model = PNN(config=config)
155+
model.to(device)
156+
model.eval()
157+
return model
158+
159+
160+
@pytest.fixture
161+
def simple_mlp(device):
162+
"""Create a simple MLP for testing."""
163+
model = nn.Sequential(
164+
nn.Linear(100, 50),
165+
nn.ReLU(),
166+
nn.Dropout(0.2),
167+
nn.Linear(50, 25),
168+
nn.ReLU(),
169+
nn.Linear(25, 1),
170+
)
171+
model.to(device)
172+
return model
173+
174+
175+
# ============================================================================
176+
# OPTIMIZER AND SCHEDULER FIXTURES
177+
# ============================================================================
178+
179+
@pytest.fixture
180+
def optimizer(simple_mlp):
181+
"""Create an optimizer for testing."""
182+
return torch.optim.Adam(simple_mlp.parameters(), lr=1e-3)
183+
184+
185+
@pytest.fixture
186+
def scheduler(optimizer):
187+
"""Create a learning rate scheduler for testing."""
188+
return torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
189+
190+
191+
# ============================================================================
192+
# DATASET AND DATALOADER FIXTURES
193+
# ============================================================================
194+
195+
@pytest.fixture
196+
def dummy_dataset(device, dtype):
197+
"""Create a simple dummy dataset for testing."""
198+
class DummyDataset(torch.utils.data.Dataset):
199+
def __init__(self, size=32, prot_dim=256, chem_dim=2048):
200+
self.size = size
201+
self.prot_dim = prot_dim
202+
self.chem_dim = chem_dim
203+
204+
def __len__(self):
205+
return self.size
206+
207+
def __getitem__(self, idx):
208+
prot = torch.randn(self.prot_dim, device=device, dtype=dtype)
209+
chem = torch.randn(self.chem_dim, device=device, dtype=dtype)
210+
target = torch.randn(1, device=device, dtype=dtype)
211+
return (prot, chem), target
212+
213+
return DummyDataset()
214+
215+
216+
@pytest.fixture
217+
def dummy_dataloader(dummy_dataset):
218+
"""Create a DataLoader from the dummy dataset."""
219+
return torch.utils.data.DataLoader(dummy_dataset, batch_size=4, shuffle=False)
220+
221+
222+
# ============================================================================
223+
# LOSS FUNCTION FIXTURES
224+
# ============================================================================
225+
226+
@pytest.fixture
227+
def nig_parameters(batch_targets, device, dtype):
228+
"""Create NIG parameters for testing."""
229+
batch_size = batch_targets.shape[0]
230+
return {
231+
"mu": torch.randn(batch_size, 1, device=device, dtype=dtype),
232+
"v": torch.relu(torch.randn(batch_size, 1, device=device, dtype=dtype)) + 0.1,
233+
"alpha": torch.relu(torch.randn(batch_size, 1, device=device, dtype=dtype)) + 1.1,
234+
"beta": torch.relu(torch.randn(batch_size, 1, device=device, dtype=dtype)) + 0.1,
235+
"y": batch_targets,
236+
}
237+
238+
239+
@pytest.fixture
240+
def dirichlet_parameters(device, dtype):
241+
"""Create Dirichlet parameters for testing."""
242+
batch_size = 8
243+
num_classes = 2
244+
return {
245+
"alpha": torch.relu(torch.randn(batch_size, num_classes, device=device, dtype=dtype)) + 0.5,
246+
"y": torch.nn.functional.one_hot(torch.randint(0, num_classes, (batch_size,)), num_classes=num_classes).float().to(device),
247+
}
248+
249+
250+
# ============================================================================
251+
# UTILITY FUNCTION FIXTURES
252+
# ============================================================================
253+
254+
@pytest.fixture
255+
def mock_config_dir(tmp_path):
256+
"""Create a temporary directory with mock config files."""
257+
config_dir = tmp_path / "config"
258+
config_dir.mkdir()
259+
return config_dir
260+
261+
262+
@pytest.fixture
263+
def mock_model_dir(tmp_path):
264+
"""Create a temporary directory for model artifacts."""
265+
model_dir = tmp_path / "models"
266+
model_dir.mkdir()
267+
return model_dir
268+
269+
270+
# ============================================================================
271+
# SEED FIXTURES FOR DETERMINISM
272+
# ============================================================================
273+
274+
@pytest.fixture(autouse=True)
275+
def reset_seed():
276+
"""Reset random seed before and after each test."""
277+
um.set_seed(42)
278+
yield
279+
um.set_seed(42)
280+
281+
282+
# ============================================================================
283+
# CONTEXT MANAGERS AND UTILITIES
284+
# ============================================================================
285+
286+
@pytest.fixture
287+
def no_wandb():
288+
"""Context manager to mock wandb during tests."""
289+
with patch("wandb.log"):
290+
yield
291+
292+
293+
@pytest.fixture
294+
def mock_device():
295+
"""Mock device operations for testing without GPU."""
296+
with patch("torch.cuda.is_available", return_value=False):
297+
yield
298+
299+
300+
# ============================================================================
301+
# CUSTOM MARKERS
302+
# ============================================================================
303+
304+
def pytest_configure(config):
305+
"""Register custom pytest markers."""
306+
config.addinivalue_line(
307+
"markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
308+
)
309+
config.addinivalue_line(
310+
"markers", "integration: marks tests as integration tests"
311+
)
312+
config.addinivalue_line(
313+
"markers", "gpu: marks tests that require GPU"
314+
)
315+
config.addinivalue_line(
316+
"markers", "unit: marks tests as unit tests"
317+
)
318+
319+
320+
# ============================================================================
321+
# HELPER UTILITIES
322+
# ============================================================================
323+
324+
def assert_tensor_shape(tensor: torch.Tensor, expected_shape: Tuple[int, ...]):
325+
"""Assert tensor has expected shape."""
326+
assert tensor.shape == expected_shape, (
327+
f"Expected shape {expected_shape}, got {tensor.shape}"
328+
)
329+
330+
331+
def assert_tensor_dtype(tensor: torch.Tensor, expected_dtype: torch.dtype):
332+
"""Assert tensor has expected dtype."""
333+
assert tensor.dtype == expected_dtype, (
334+
f"Expected dtype {expected_dtype}, got {tensor.dtype}"
335+
)
336+
337+
338+
def assert_finite(tensor: torch.Tensor):
339+
"""Assert tensor contains no NaN or Inf values."""
340+
assert torch.isfinite(tensor).all(), (
341+
f"Tensor contains NaN or Inf values"
342+
)
343+
344+
345+
def assert_grad_flow(tensor: torch.Tensor):
346+
"""Assert that gradient exists for backward pass."""
347+
assert tensor.grad is not None, "No gradient computed"
348+
349+
350+
import pytest
351+
352+
# Provide dropout_rate fixture for parametrized unittest methods
353+
@pytest.fixture(params=[0.0, 0.1, 0.3, 0.5])
354+
def dropout_rate(request):
355+
return request.param

0 commit comments

Comments
 (0)