Skip to content

Commit afd2cf9

Browse files
committed
[Move DISCO queue to core]:
- Move load_anchor_points to DISCOQueue
1 parent 2693197 commit afd2cf9

3 files changed

Lines changed: 147 additions & 47 deletions

File tree

maseval/benchmark/mmlu/mmlu.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,9 @@
2424
"""
2525

2626
import json
27-
import pickle
2827
from pathlib import Path
2928
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
3029

31-
# numpy is optional - only needed for anchor points processing
32-
try:
33-
import numpy as np
34-
35-
HAS_NUMPY = True
36-
except ImportError:
37-
np = None # type: ignore[assignment]
38-
HAS_NUMPY = False
39-
4030
from maseval import (
4131
AgentAdapter,
4232
DISCOQueue,
@@ -561,29 +551,6 @@ def get_model_adapter(self, model_id: str, **kwargs: Any) -> ModelAdapter:
561551
# =============================================================================
562552

563553

564-
def load_pickle(path: Union[str, Path]) -> Any:
565-
"""Load a pickle file."""
566-
with open(path, "rb") as f:
567-
return pickle.load(f)
568-
569-
570-
def load_anchor_points(path: Union[str, Path]) -> List[int]:
571-
"""Load anchor points from a .json or .pkl file. Returns a list of doc_ids."""
572-
path = Path(path)
573-
if not path.exists():
574-
raise FileNotFoundError(f"Anchor points file not found: {path}")
575-
if path.suffix.lower() == ".json":
576-
with open(path) as f:
577-
anchor_points = json.load(f)
578-
else:
579-
anchor_points = load_pickle(path)
580-
if HAS_NUMPY and isinstance(anchor_points, np.ndarray):
581-
anchor_points = anchor_points.tolist()
582-
elif not HAS_NUMPY and hasattr(anchor_points, "tolist"):
583-
anchor_points = anchor_points.tolist()
584-
return list(anchor_points)
585-
586-
587554
def load_tasks(
588555
data_path: Union[str, Path],
589556
anchor_points_path: Optional[Union[str, Path]] = None,
@@ -601,8 +568,6 @@ def load_tasks(
601568
Returns:
602569
TaskQueue containing MMLU tasks.
603570
604-
Raises:
605-
ImportError: If anchor_points_path is provided but numpy is not installed.
606571
"""
607572
data_path = Path(data_path)
608573

@@ -642,14 +607,8 @@ def load_tasks(
642607
)
643608
tasks.append(task)
644609

645-
# Load anchor points if provided
646-
anchor_points = None
647610
if anchor_points_path is not None:
648-
anchor_points = load_anchor_points(anchor_points_path)
649-
650-
# Create appropriate queue
651-
if anchor_points is not None:
652-
return DISCOQueue(tasks, anchor_points)
611+
return DISCOQueue(tasks, anchor_points_path=anchor_points_path)
653612
else:
654613
return SequentialTaskQueue(tasks)
655614

maseval/core/task.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import Sequence
66
from typing import Iterable, List, Union, Iterator, Optional
77
import json
8+
import pickle
89
from pathlib import Path
910
from enum import Enum
1011

@@ -339,25 +340,75 @@ class DISCOQueue(InformativeSubsetQueue):
339340
Example:
340341
```python
341342
queue = DISCOQueue(tasks, anchor_points=[0, 5, 12])
343+
# or load from file:
344+
queue = DISCOQueue(tasks, anchor_points_path="anchor_points.pkl")
342345
343346
for task in queue:
344-
result = execute(task) # Only 3 tasks
347+
result = execute(task) # Only anchor-point tasks
345348
```
346349
"""
347350

348-
def __init__(self, tasks: Iterable[Task], anchor_points: Optional[List[int]] = None) -> None:
351+
def __init__(
352+
self,
353+
tasks: Iterable[Task],
354+
anchor_points: Optional[List[int]] = None,
355+
anchor_points_path: Optional[Union[str, Path]] = None,
356+
) -> None:
349357
"""Initialize DISCO task queue.
350358
359+
Anchor points can be supplied directly via ``anchor_points`` or loaded
360+
from a file via ``anchor_points_path``. Providing both is an error.
361+
351362
Args:
352363
tasks: Full list of tasks (ordered by index).
353364
anchor_points: Diversity-selected indices into ``tasks``.
354-
Typically loaded from a DISCO anchor-points file or
355-
downloaded from a HuggingFace DISCO model repo.
356-
If ``None``, evaluates all tasks in order.
365+
Typically downloaded from a HuggingFace DISCO model repo.
366+
If ``None`` and ``anchor_points_path`` is also ``None``,
367+
evaluates all tasks in order.
368+
anchor_points_path: Path to a ``.json`` or ``.pkl`` file
369+
containing anchor-point indices. Mutually exclusive with
370+
``anchor_points``.
357371
"""
372+
if anchor_points is not None and anchor_points_path is not None:
373+
raise ValueError("Provide either anchor_points or anchor_points_path, not both.")
374+
375+
if anchor_points_path is not None:
376+
anchor_points = self.load_anchor_points(anchor_points_path)
377+
358378
self._anchor_points: Optional[List[int]] = anchor_points
359379
super().__init__(tasks, indices=anchor_points)
360380

381+
@staticmethod
382+
def load_anchor_points(path: Union[str, Path]) -> List[int]:
383+
"""Load anchor points from a ``.json`` or ``.pkl`` file.
384+
385+
Args:
386+
path: Path to anchor points file. JSON files should contain a
387+
list of integer indices. Pickle files may contain a list or
388+
a numpy array.
389+
390+
Returns:
391+
List of integer anchor-point indices.
392+
393+
Raises:
394+
FileNotFoundError: If the file does not exist.
395+
"""
396+
path = Path(path)
397+
if not path.exists():
398+
raise FileNotFoundError(f"Anchor points file not found: {path}")
399+
400+
if path.suffix.lower() == ".json":
401+
with open(path) as f:
402+
anchor_points = json.load(f)
403+
else:
404+
with open(path, "rb") as f:
405+
anchor_points = pickle.load(f)
406+
407+
if hasattr(anchor_points, "tolist"):
408+
anchor_points = anchor_points.tolist()
409+
410+
return list(anchor_points)
411+
361412

362413
class PriorityTaskQueue(BaseTaskQueue):
363414
"""Execute tasks ordered by priority.

tests/test_core/test_queue.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
)
2121

2222

23+
class _FakeArray:
24+
"""Pickle-serializable array-like for testing .tolist() conversion."""
25+
26+
def tolist(self):
27+
return [1, 2, 3]
28+
29+
def __iter__(self):
30+
return iter([1, 2, 3])
31+
32+
2333
# ==================== Fixtures ====================
2434

2535

@@ -318,6 +328,86 @@ def test_len_matches_anchor_count(self):
318328
assert len(queue) == 3
319329

320330

331+
@pytest.mark.core
332+
class TestDISCOQueueLoadAnchorPoints:
333+
"""Tests for DISCOQueue.load_anchor_points static method."""
334+
335+
def test_load_from_json(self, tmp_path):
336+
"""Should load anchor points from a JSON file."""
337+
import json
338+
339+
path = tmp_path / "anchors.json"
340+
path.write_text(json.dumps([0, 5, 12, 99]))
341+
342+
result = DISCOQueue.load_anchor_points(path)
343+
344+
assert result == [0, 5, 12, 99]
345+
346+
def test_load_from_pickle(self, tmp_path):
347+
"""Should load anchor points from a pickle file."""
348+
import pickle
349+
350+
path = tmp_path / "anchors.pkl"
351+
with open(path, "wb") as f:
352+
pickle.dump([2, 7, 15], f)
353+
354+
result = DISCOQueue.load_anchor_points(path)
355+
356+
assert result == [2, 7, 15]
357+
358+
def test_load_converts_tolist(self, tmp_path):
359+
"""Should call .tolist() on array-like objects (e.g. numpy arrays)."""
360+
import pickle
361+
362+
path = tmp_path / "anchors.pkl"
363+
with open(path, "wb") as f:
364+
pickle.dump(_FakeArray(), f)
365+
366+
result = DISCOQueue.load_anchor_points(path)
367+
368+
assert result == [1, 2, 3]
369+
370+
def test_file_not_found(self, tmp_path):
371+
"""Should raise FileNotFoundError for missing files."""
372+
with pytest.raises(FileNotFoundError, match="not found"):
373+
DISCOQueue.load_anchor_points(tmp_path / "nonexistent.json")
374+
375+
def test_accepts_string_path(self, tmp_path):
376+
"""Should accept a string path, not just Path objects."""
377+
import json
378+
379+
path = tmp_path / "anchors.json"
380+
path.write_text(json.dumps([10, 20]))
381+
382+
result = DISCOQueue.load_anchor_points(str(path))
383+
384+
assert result == [10, 20]
385+
386+
def test_init_with_anchor_points_path(self, tmp_path):
387+
"""DISCOQueue should load anchor points from file when anchor_points_path is given."""
388+
import json
389+
390+
tasks = [Task(query=f"Q{i}") for i in range(10)]
391+
path = tmp_path / "anchors.json"
392+
path.write_text(json.dumps([2, 5, 8]))
393+
394+
queue = DISCOQueue(tasks, anchor_points_path=path)
395+
396+
assert len(queue) == 3
397+
assert queue._anchor_points == [2, 5, 8]
398+
399+
def test_init_rejects_both_anchor_args(self, tmp_path):
400+
"""DISCOQueue should raise ValueError when both anchor_points and anchor_points_path are given."""
401+
import json
402+
403+
tasks = [Task(query=f"Q{i}") for i in range(5)]
404+
path = tmp_path / "anchors.json"
405+
path.write_text(json.dumps([0, 1]))
406+
407+
with pytest.raises(ValueError, match="not both"):
408+
DISCOQueue(tasks, anchor_points=[0, 1], anchor_points_path=path)
409+
410+
321411
# ==================== PriorityTaskQueue Tests ====================
322412

323413

0 commit comments

Comments
 (0)