rpx_benchmark.loader

Data loading utilities for RPX benchmark.

Reads a JSON manifest describing a slice of the RPX dataset (usually produced by rpx_benchmark.hub.download_split()) and yields ~rpx_benchmark.api.Sample batches the BenchmarkRunner can feed to a model.

Manifest format

::

{
  "task":   "monocular_depth",
  "root":   "/absolute/path/or/snapshot/root",
  "samples": [
    {
      "id":         "scene_001_clutter_00000",
      "scene":      "scene_001",
      "phase":      "clutter",
      "difficulty": "hard",
      "rgb":   "scenes/scene_001/0/rgb/00000.png",
      "depth": "scenes/scene_001/0/depth/00000.png",
      "mask":  "scenes/scene_001/0/mask/00000.png",
      "pose":  "scenes/scene_001/0/pose/00000.npz"
    },
    ...
  ]
}

All sample paths are resolved relative to root unless they are absolute. depth files are 16-bit PNGs in millimetres (as saved by save_device_data.py); the loader converts to float32 metres on read. pose files are .npz with position and orientation (T265 convention, [x, y, z, w] quaternion).

Errors

All load-time failures are raised as ~rpx_benchmark.exceptions.ManifestError so user code can catch them specifically.

  1"""Data loading utilities for RPX benchmark.
  2
  3Reads a JSON *manifest* describing a slice of the RPX dataset (usually
  4produced by :func:`rpx_benchmark.hub.download_split`) and yields
  5:class:`~rpx_benchmark.api.Sample` batches the :class:`BenchmarkRunner`
  6can feed to a model.
  7
  8Manifest format
  9---------------
 10
 11::
 12
 13    {
 14      "task":   "monocular_depth",
 15      "root":   "/absolute/path/or/snapshot/root",
 16      "samples": [
 17        {
 18          "id":         "scene_001_clutter_00000",
 19          "scene":      "scene_001",
 20          "phase":      "clutter",
 21          "difficulty": "hard",
 22          "rgb":   "scenes/scene_001/0/rgb/00000.png",
 23          "depth": "scenes/scene_001/0/depth/00000.png",
 24          "mask":  "scenes/scene_001/0/mask/00000.png",
 25          "pose":  "scenes/scene_001/0/pose/00000.npz"
 26        },
 27        ...
 28      ]
 29    }
 30
 31All sample paths are resolved relative to ``root`` unless they are
 32absolute. ``depth`` files are 16-bit PNGs in millimetres (as saved by
 33``save_device_data.py``); the loader converts to float32 metres on
 34read. ``pose`` files are ``.npz`` with ``position`` and ``orientation``
 35(T265 convention, ``[x, y, z, w]`` quaternion).
 36
 37Errors
 38------
 39
 40All load-time failures are raised as
 41:class:`~rpx_benchmark.exceptions.ManifestError` so user code can
 42catch them specifically.
 43"""
 44
 45from __future__ import annotations
 46
 47import json
 48from dataclasses import dataclass
 49from pathlib import Path
 50from typing import Any, Dict, Iterator, List
 51
 52import numpy as np
 53
 54from .api import (
 55    DepthGroundTruth,
 56    DetectionGroundTruth,
 57    Difficulty,
 58    KeypointCorrespondenceGroundTruth,
 59    Phase,
 60    RelativePoseGroundTruth,
 61    Sample,
 62    SegmentationGroundTruth,
 63    SparseDepthGroundTruth,
 64    TaskType,
 65    Tracklet,
 66    TrackletGroundTruth,
 67    VisualGroundingGroundTruth,
 68)
 69from .exceptions import ManifestError
 70from .logging_utils import get_logger
 71
 72log = get_logger(__name__)
 73
 74# Phase index → Phase enum (matches capture session folder 0/1/2)
 75_PHASE_INDEX: Dict[int, Phase] = {
 76    0: Phase.CLUTTER,
 77    1: Phase.INTERACTION,
 78    2: Phase.CLEAN,
 79}
 80
 81
 82@dataclass
 83class RPXDataset:
 84    """Iterates over RPX samples for a specific task.
 85
 86    Manifest format (JSON)::
 87
 88        {
 89          "task": "object_segmentation",
 90          "root": "/path/to/data",
 91          "samples": [
 92            {
 93              "id": "scene_001_clutter_00000",
 94              "scene": "scene_001",
 95              "phase": "clutter",
 96              "difficulty": "hard",
 97              "rgb":   "scene_001/0/rgb/00000.png",
 98              "depth": "scene_001/0/depth/00000.png",
 99              "mask":  "scene_001/0/mask/00000.png",
100              "pose":  "scene_001/0/pose/00000.npz",
101              ...
102            }
103          ]
104        }
105
106    All paths are relative to ``root``.
107    ``depth`` files are 16-bit PNG in millimetres (as saved by save_device_data.py).
108    ``pose`` files are NPZ with keys ``position`` ([x,y,z] metres) and
109    ``orientation`` ([x,y,z,w] quaternion) from the T265 tracker.
110    """
111
112    samples: List[Dict[str, Any]]
113    task: TaskType
114    root: Path
115    batch_size: int = 1
116
117    @classmethod
118    def from_manifest(
119        cls,
120        manifest_path: str | Path,
121        batch_size: int = 1,
122        validate: bool = False,
123        max_samples: int | None = None,
124    ) -> "RPXDataset":
125        """Load a manifest JSON file from disk and return a dataset.
126
127        Parameters
128        ----------
129        manifest_path : str or Path
130            Path to the manifest JSON. Produced either by
131            :func:`rpx_benchmark.hub.download_split` or by a custom
132            upload script.
133        batch_size : int
134            Number of samples per iteration. Default 1.
135        validate : bool, optional
136            When ``True``, validate the manifest against the strict
137            Pydantic schema in :mod:`rpx_benchmark.schemas` before
138            constructing the dataset. Requires the ``schemas`` extra
139            (``pip install 'rpx-benchmark[schemas]'``). Default
140            ``False`` preserves historical, tolerant behaviour.
141        max_samples : int, optional
142            Keep only the first N manifest samples. Intended for smoke
143            validation; must be at least 1 when provided.
144
145        Returns
146        -------
147        RPXDataset
148
149        Raises
150        ------
151        ManifestError
152            If the manifest file is missing, not valid JSON, is missing
153            required top-level fields, or (when ``validate=True``)
154            fails strict schema validation.
155        """
156        manifest_path = Path(manifest_path)
157        if not manifest_path.is_file():
158            raise ManifestError(
159                f"Manifest file not found: {manifest_path}",
160                hint="Did the HuggingFace download fail? Try rerunning with "
161                "--cache-dir pointing at a writable location.",
162            )
163        try:
164            with manifest_path.open("r", encoding="utf-8") as f:
165                manifest = json.load(f)
166        except json.JSONDecodeError as e:
167            raise ManifestError(
168                f"Manifest at {manifest_path} is not valid JSON: {e}",
169            ) from e
170        dataset = cls.from_dict(
171            manifest,
172            batch_size=batch_size,
173            default_root=manifest_path.parent,
174            validate=validate,
175        )
176        if max_samples is not None:
177            if max_samples < 1:
178                raise ManifestError(
179                    f"max_samples must be >= 1, got {max_samples}",
180                )
181            dataset.samples = dataset.samples[:max_samples]
182        return dataset
183
184    @classmethod
185    def from_dict(
186        cls,
187        manifest: Dict[str, Any],
188        batch_size: int = 1,
189        default_root: str | Path | None = None,
190        validate: bool = False,
191    ) -> "RPXDataset":
192        """Build a dataset from an already-parsed manifest dict.
193
194        Raises
195        ------
196        ManifestError
197            If ``task`` is missing or unknown, or if ``samples`` is
198            missing. When ``validate=True``, any Pydantic validation
199            error is also surfaced as :class:`ManifestError`.
200        """
201        if validate:
202            # Opt-in strict validation via rpx_benchmark.schemas. Kept
203            # lazy so users who don't install the ``schemas`` extra
204            # don't hit pydantic import at module load time.
205            from .schemas import validate_manifest  # noqa: PLC0415 — lazy import
206
207            model = validate_manifest(manifest)
208            manifest = model.model_dump(exclude_none=True)
209
210        if "task" not in manifest:
211            raise ManifestError(
212                "Manifest is missing required field 'task'.",
213                hint="Task must be one of: " + ", ".join(t.value for t in TaskType),
214            )
215        try:
216            task = TaskType(manifest["task"])
217        except ValueError as e:
218            raise ManifestError(
219                f"Manifest task {manifest['task']!r} is not a known TaskType.",
220                hint="Expected one of: " + ", ".join(t.value for t in TaskType),
221            ) from e
222
223        if "samples" not in manifest:
224            raise ManifestError(
225                "Manifest is missing required field 'samples'.",
226            )
227
228        root = Path(manifest.get("root") or default_root or ".")
229        samples = manifest["samples"]
230        if not isinstance(samples, list):
231            raise ManifestError(
232                f"Manifest 'samples' must be a list, got {type(samples).__name__}",
233            )
234        log.debug("loaded manifest: task=%s root=%s samples=%d", task.value, root, len(samples))
235        return cls(samples=samples, task=task, root=root, batch_size=batch_size)
236
237    @classmethod
238    def from_hf(
239        cls,
240        hf_dataset: Any,
241        task: TaskType | str | None = None,
242        batch_size: int = 1,
243    ) -> Any:
244        """Wrap a ``datasets.Dataset`` as an iterable RPX source.
245
246        Returns a :class:`rpx_benchmark.data.hf_bridge.RPXHFBridge`
247        which is interchangeable with :class:`RPXDataset` at the
248        runner's consumer interface (``__len__`` + ``__iter__`` over
249        ``list[Sample]``).
250
251        Parameters
252        ----------
253        hf_dataset : datasets.Dataset
254            Dataset whose columns match
255            :func:`rpx_benchmark.data.features.features_for_task`.
256        task : TaskType or str, optional
257            Task this dataset serves. If omitted, we try to infer it
258            from ``hf_dataset.info.description`` (where
259            :func:`rpx_benchmark.data.load_hf` stamps it).
260        batch_size : int, default 1
261
262        Returns
263        -------
264        RPXHFBridge
265
266        Raises
267        ------
268        ManifestError
269            If ``task`` is neither provided nor discoverable on the
270            dataset metadata.
271        """
272        from .data.hf_bridge import RPXHFBridge  # noqa: PLC0415 — lazy
273
274        if task is None:
275            info = getattr(hf_dataset, "info", None)
276            config = getattr(info, "config_name", None) if info is not None else None
277            if config is None:
278                raise ManifestError(
279                    "Could not infer task from the HF dataset; pass task=TaskType.* explicitly.",
280                )
281            task = TaskType(config)
282        if isinstance(task, str):
283            task = TaskType(task)
284        return RPXHFBridge(hf_dataset=hf_dataset, task=task, batch_size=batch_size)
285
286    def __len__(self) -> int:
287        return len(self.samples)
288
289    def __iter__(self) -> Iterator[List[Sample]]:
290        batch: List[Sample] = []
291        for entry in self.samples:
292            batch.append(self._load_sample(entry))
293            if len(batch) == self.batch_size:
294                yield batch
295                batch = []
296        if batch:
297            yield batch
298
299    def _load_sample(self, entry: Dict[str, Any]) -> Sample:
300        rgb = self._load_rgb(entry["rgb"])
301
302        # Phase — from string label or integer session index
303        phase: Phase | None = None
304        if "phase" in entry:
305            v = entry["phase"]
306            phase = Phase(v) if isinstance(v, str) else _PHASE_INDEX.get(int(v))
307
308        # Difficulty
309        difficulty: Difficulty | None = None
310        if "difficulty" in entry:
311            difficulty = Difficulty(entry["difficulty"])
312
313        # Camera pose (T265 NPZ → 4×4 SE(3) float64)
314        camera_pose: np.ndarray | None = None
315        if "pose" in entry:
316            camera_pose = self._load_pose(entry["pose"])
317
318        # Fisheye images — stored in metadata if present
319        metadata: Dict[str, Any] = dict(entry.get("metadata") or {})
320        scene = entry.get("scene_id") or entry.get("scene")
321        if scene is not None:
322            metadata["scene_id"] = scene
323        if "fisheye_left" in entry:
324            metadata["fisheye_left"] = self._load_gray(entry["fisheye_left"])
325        if "fisheye_right" in entry:
326            metadata["fisheye_right"] = self._load_gray(entry["fisheye_right"])
327        # Second RGB frame for pair tasks (relative pose, keypoint matching)
328        if "rgb_b" in entry:
329            metadata["rgb_b"] = self._load_rgb(entry["rgb_b"])
330
331        gt = self._load_ground_truth(entry)
332
333        return Sample(
334            id=entry["id"],
335            rgb=rgb,
336            ground_truth=gt,
337            metadata=metadata or None,
338            phase=phase,
339            difficulty=difficulty,
340            camera_pose=camera_pose,
341        )
342
343    def _load_ground_truth(self, entry: Dict[str, Any]) -> Any:
344        task = self.task
345
346        if task == TaskType.MONOCULAR_DEPTH:
347            return DepthGroundTruth(depth_map=self._load_depth(entry["depth"]))
348
349        if task in (TaskType.OBJECT_DETECTION, TaskType.OPEN_VOCAB_DETECTION):
350            boxes, labels = self._load_boxes(entry["boxes"])
351            return DetectionGroundTruth(boxes=boxes, labels=labels)
352
353        if task == TaskType.OBJECT_SEGMENTATION:
354            return SegmentationGroundTruth(mask=self._load_mask(entry["mask"]))
355
356        if task == TaskType.OBJECT_TRACKING:
357            if "tracks" in entry and self._resolve(entry["tracks"]).is_file():
358                return self._load_tracklets(entry)
359            return self._load_tracking_mask(entry)
360
361        if task == TaskType.RELATIVE_CAMERA_POSE:
362            return self._load_relative_pose(entry)
363
364        if task == TaskType.VISUAL_GROUNDING:
365            return self._load_visual_grounding(entry)
366
367        if task == TaskType.SPARSE_DEPTH:
368            return self._load_sparse_depth(entry)
369
370
371        if task == TaskType.KEYPOINT_MATCHING:
372            return self._load_keypoints(entry)
373
374        raise ManifestError(
375            f"Unsupported task {task} — loader does not know how to parse "
376            "its ground-truth entries.",
377        )
378
379    # ------------------------------------------------------------------ #
380    # Ground-truth loaders
381    # ------------------------------------------------------------------ #
382
383    def _load_tracklets(self, entry: Dict[str, Any]) -> TrackletGroundTruth:
384        path = self._resolve(entry["tracks"])
385        with path.open("r", encoding="utf-8") as f:
386            data = json.load(f)
387        tracks = []
388        for item in data:
389            boxes = np.array(item["boxes"], dtype=np.float32)
390            scores = np.array(item["scores"], dtype=np.float32) if "scores" in item else None
391            tracks.append(Tracklet(track_id=str(item["track_id"]), boxes=boxes, scores=scores))
392        return TrackletGroundTruth(tracks=tracks)
393
394    def _load_tracking_mask(self, entry: Dict[str, Any]) -> TrackletGroundTruth:
395        """Represent one released instance-mask frame as one-step tracklets.
396
397        The D3 production runner groups the masks into complete clips before
398        evaluation.  This fallback keeps the generic frame loader usable for
399        inspection without depending on the unpublished ``tracklets/v1.json``.
400        """
401        mask = self._load_mask(entry["mask"])
402        tracks = []
403        for track_id in np.unique(mask):
404            if int(track_id) <= 0:
405                continue
406            ys, xs = np.nonzero(mask == track_id)
407            if xs.size == 0:
408                continue
409            boxes = np.array(
410                [[xs.min(), ys.min(), xs.max() + 1, ys.max() + 1]],
411                dtype=np.float32,
412            )
413            tracks.append(Tracklet(track_id=str(int(track_id)), boxes=boxes))
414        return TrackletGroundTruth(tracks=tracks)
415
416    def _load_relative_pose(self, entry: Dict[str, Any]) -> RelativePoseGroundTruth:
417        from .pose_conventions import relative_pose_from_raw_t265
418
419        # Published RPX poses retain librealsense's T265 axes. RCPE models
420        # consume D435 RGB and conventionally emit OpenCV camera coordinates,
421        # so convert the fixed axis basis before forming the relative pose.
422        pose_a = self._load_pose(entry["pose_a"])  # 4×4 SE(3)
423        pose_b = self._load_pose(entry["pose_b"])
424        T_rel = relative_pose_from_raw_t265(pose_a, pose_b)
425        return RelativePoseGroundTruth(
426            rotation=T_rel[:3, :3],
427            translation=T_rel[:3, 3],
428        )
429
430    def _load_visual_grounding(self, entry: Dict[str, Any]) -> VisualGroundingGroundTruth:
431        text = entry["text"]
432        boxes_raw = entry.get("boxes")
433        labels = entry.get("labels")
434        if boxes_raw is None:
435            boxes = np.zeros((0, 4), dtype=np.float32)
436        elif isinstance(boxes_raw, str):
437            gt_boxes, _ = self._load_boxes(boxes_raw)
438            boxes = gt_boxes
439        else:
440            boxes = np.array(boxes_raw, dtype=np.float32)
441        return VisualGroundingGroundTruth(text=text, boxes=boxes, labels=labels)
442
443    def _load_sparse_depth(self, entry: Dict[str, Any]) -> SparseDepthGroundTruth:
444        coordinates = self._load_array_or_inline(entry["coordinates"]).astype(np.float32)
445        depths = self._load_array_or_inline(entry["depths"]).astype(np.float32)
446        return SparseDepthGroundTruth(coordinates=coordinates, depths=depths)
447
448
449    def _load_keypoints(self, entry: Dict[str, Any]) -> KeypointCorrespondenceGroundTruth:
450        points0 = self._load_array_or_inline(entry["points0"]).astype(np.float32)
451        points1 = self._load_array_or_inline(entry["points1"]).astype(np.float32)
452        visibility = None
453        if "visibility" in entry:
454            visibility = self._load_array_or_inline(entry["visibility"]).astype(bool)
455        return KeypointCorrespondenceGroundTruth(
456            points0=points0, points1=points1, visibility=visibility
457        )
458
459    # ------------------------------------------------------------------ #
460    # Low-level file loaders
461    # ------------------------------------------------------------------ #
462
463    def _resolve(self, relative_path: str | Path) -> Path:
464        path = Path(relative_path)
465        return path if path.is_absolute() else self.root / path
466
467    def _load_rgb(self, relative_path: str) -> np.ndarray:
468        """Load an RGB image → ``(H, W, 3) uint8``.
469
470        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_rgb`
471        so the dtype/shape contract is enforced in exactly one place,
472        shared with every adapter that decodes directly.
473        """
474        from .decode_contracts import safe_load_rgb
475
476        return safe_load_rgb(self._resolve(relative_path))
477
478    def _load_depth(self, relative_path: str) -> np.ndarray:
479        """Load a 16-bit PNG depth file → ``(H, W) float32`` in metres.
480
481        Decoding is delegated to
482        :func:`rpx_benchmark.decode_contracts.safe_load_depth`, which
483        guarantees ``uint16`` (the only correct decode for 16-bit
484        depth PNGs). We then cast to float32 and scale mm → m.
485        """
486        from .decode_contracts import safe_load_depth
487
488        raw = safe_load_depth(self._resolve(relative_path))  # (H, W) uint16
489        depth_mm = raw.astype(np.float32)
490        depth_m = depth_mm / 1000.0
491        depth_m[depth_mm == 0] = 0.0  # keep 0 as "invalid" sentinel
492        return depth_m
493
494    def _load_pose(self, relative_path: str) -> np.ndarray:
495        """Load a T265 pose → 4×4 SE(3) float64 (camera-to-world).
496
497        Two on-disk formats are accepted:
498
499        * Legacy ``.npz`` (per-frame zip): ``position`` (3,) +
500          ``orientation`` (4, xyzw quaternion).
501        * Compact ``.npy`` (per-frame raw, produced by the
502          ``lossless-convert`` step): a single ``(7,) float64`` vector
503          packing ``[x, y, z, qx, qy, qz, qw]``. The values are
504          bit-identical to the legacy ``.npz`` form.
505        """
506        path = self._resolve(relative_path)
507        data = np.load(path)
508        if path.suffix.lower() == ".npy":
509            arr = np.asarray(data, dtype=np.float64)
510            if arr.shape != (7,):
511                raise ManifestError(
512                    f"Pose .npy at {path} must be shape (7,) float64; got {arr.shape}",
513                    hint="The dataset_hub lossless-convert step writes a "
514                    "single (7,) vector packing [x, y, z, qx, qy, qz, qw].",
515                )
516            position = arr[:3]
517            quat_xyzw = arr[3:]
518        else:
519            position = data["position"].astype(np.float64)  # (3,)
520            quat_xyzw = data["orientation"].astype(np.float64)  # (4,) xyzw
521
522        R = _quat_xyzw_to_rotmat(quat_xyzw)
523        T = np.eye(4, dtype=np.float64)
524        T[:3, :3] = R
525        T[:3, 3] = position
526        if T.shape != (4, 4) or T.dtype != np.float64:
527            raise ManifestError(
528                f"Pose load contract violated at {path}: "
529                f"expected (4, 4) float64, got shape {T.shape} dtype {T.dtype}",
530            )
531        return T
532
533    def _load_mask(self, relative_path: str) -> np.ndarray:
534        """Load an instance-segmentation mask → ``(H, W) int32``.
535
536        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_mask`.
537        """
538        from .decode_contracts import safe_load_mask
539
540        return safe_load_mask(self._resolve(relative_path))
541
542    def _load_gray(self, relative_path: str) -> np.ndarray:
543        """Load a grayscale image (fisheye) → ``(H, W) uint8``.
544
545        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_gray`.
546        """
547        from .decode_contracts import safe_load_gray
548
549        return safe_load_gray(self._resolve(relative_path))
550
551    def _load_boxes(self, relative_path: str):
552        path = self._resolve(relative_path)
553        with path.open("r", encoding="utf-8") as f:
554            data = json.load(f)
555        boxes = np.array([item["bbox"] for item in data], dtype=np.float32)
556        labels = [item["label"] for item in data]
557        return boxes, labels
558
559    def _load_array_or_inline(self, value: Any) -> np.ndarray:
560        if isinstance(value, str):
561            path = self._resolve(value)
562            if path.suffix.lower() == ".npy":
563                return np.load(path)
564            with path.open("r", encoding="utf-8") as f:
565                return np.array(json.load(f))
566        return np.array(value)
567
568
569# ------------------------------------------------------------------ #
570# Quaternion helper
571# ------------------------------------------------------------------ #
572
573
574def _quat_xyzw_to_rotmat(q: np.ndarray) -> np.ndarray:
575    """Convert quaternion [x, y, z, w] (T265 convention) to 3×3 rotation matrix."""
576    x, y, z, w = q / np.linalg.norm(q)
577    return np.array(
578        [
579            [1 - 2 * y * y - 2 * z * z, 2 * x * y - 2 * z * w, 2 * x * z + 2 * y * w],
580            [2 * x * y + 2 * z * w, 1 - 2 * x * x - 2 * z * z, 2 * y * z - 2 * x * w],
581            [2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x * x - 2 * y * y],
582        ],
583        dtype=np.float64,
584    )
log = <Logger rpx_benchmark.loader (WARNING)>
@dataclass
class RPXDataset:
 83@dataclass
 84class RPXDataset:
 85    """Iterates over RPX samples for a specific task.
 86
 87    Manifest format (JSON)::
 88
 89        {
 90          "task": "object_segmentation",
 91          "root": "/path/to/data",
 92          "samples": [
 93            {
 94              "id": "scene_001_clutter_00000",
 95              "scene": "scene_001",
 96              "phase": "clutter",
 97              "difficulty": "hard",
 98              "rgb":   "scene_001/0/rgb/00000.png",
 99              "depth": "scene_001/0/depth/00000.png",
100              "mask":  "scene_001/0/mask/00000.png",
101              "pose":  "scene_001/0/pose/00000.npz",
102              ...
103            }
104          ]
105        }
106
107    All paths are relative to ``root``.
108    ``depth`` files are 16-bit PNG in millimetres (as saved by save_device_data.py).
109    ``pose`` files are NPZ with keys ``position`` ([x,y,z] metres) and
110    ``orientation`` ([x,y,z,w] quaternion) from the T265 tracker.
111    """
112
113    samples: List[Dict[str, Any]]
114    task: TaskType
115    root: Path
116    batch_size: int = 1
117
118    @classmethod
119    def from_manifest(
120        cls,
121        manifest_path: str | Path,
122        batch_size: int = 1,
123        validate: bool = False,
124        max_samples: int | None = None,
125    ) -> "RPXDataset":
126        """Load a manifest JSON file from disk and return a dataset.
127
128        Parameters
129        ----------
130        manifest_path : str or Path
131            Path to the manifest JSON. Produced either by
132            :func:`rpx_benchmark.hub.download_split` or by a custom
133            upload script.
134        batch_size : int
135            Number of samples per iteration. Default 1.
136        validate : bool, optional
137            When ``True``, validate the manifest against the strict
138            Pydantic schema in :mod:`rpx_benchmark.schemas` before
139            constructing the dataset. Requires the ``schemas`` extra
140            (``pip install 'rpx-benchmark[schemas]'``). Default
141            ``False`` preserves historical, tolerant behaviour.
142        max_samples : int, optional
143            Keep only the first N manifest samples. Intended for smoke
144            validation; must be at least 1 when provided.
145
146        Returns
147        -------
148        RPXDataset
149
150        Raises
151        ------
152        ManifestError
153            If the manifest file is missing, not valid JSON, is missing
154            required top-level fields, or (when ``validate=True``)
155            fails strict schema validation.
156        """
157        manifest_path = Path(manifest_path)
158        if not manifest_path.is_file():
159            raise ManifestError(
160                f"Manifest file not found: {manifest_path}",
161                hint="Did the HuggingFace download fail? Try rerunning with "
162                "--cache-dir pointing at a writable location.",
163            )
164        try:
165            with manifest_path.open("r", encoding="utf-8") as f:
166                manifest = json.load(f)
167        except json.JSONDecodeError as e:
168            raise ManifestError(
169                f"Manifest at {manifest_path} is not valid JSON: {e}",
170            ) from e
171        dataset = cls.from_dict(
172            manifest,
173            batch_size=batch_size,
174            default_root=manifest_path.parent,
175            validate=validate,
176        )
177        if max_samples is not None:
178            if max_samples < 1:
179                raise ManifestError(
180                    f"max_samples must be >= 1, got {max_samples}",
181                )
182            dataset.samples = dataset.samples[:max_samples]
183        return dataset
184
185    @classmethod
186    def from_dict(
187        cls,
188        manifest: Dict[str, Any],
189        batch_size: int = 1,
190        default_root: str | Path | None = None,
191        validate: bool = False,
192    ) -> "RPXDataset":
193        """Build a dataset from an already-parsed manifest dict.
194
195        Raises
196        ------
197        ManifestError
198            If ``task`` is missing or unknown, or if ``samples`` is
199            missing. When ``validate=True``, any Pydantic validation
200            error is also surfaced as :class:`ManifestError`.
201        """
202        if validate:
203            # Opt-in strict validation via rpx_benchmark.schemas. Kept
204            # lazy so users who don't install the ``schemas`` extra
205            # don't hit pydantic import at module load time.
206            from .schemas import validate_manifest  # noqa: PLC0415 — lazy import
207
208            model = validate_manifest(manifest)
209            manifest = model.model_dump(exclude_none=True)
210
211        if "task" not in manifest:
212            raise ManifestError(
213                "Manifest is missing required field 'task'.",
214                hint="Task must be one of: " + ", ".join(t.value for t in TaskType),
215            )
216        try:
217            task = TaskType(manifest["task"])
218        except ValueError as e:
219            raise ManifestError(
220                f"Manifest task {manifest['task']!r} is not a known TaskType.",
221                hint="Expected one of: " + ", ".join(t.value for t in TaskType),
222            ) from e
223
224        if "samples" not in manifest:
225            raise ManifestError(
226                "Manifest is missing required field 'samples'.",
227            )
228
229        root = Path(manifest.get("root") or default_root or ".")
230        samples = manifest["samples"]
231        if not isinstance(samples, list):
232            raise ManifestError(
233                f"Manifest 'samples' must be a list, got {type(samples).__name__}",
234            )
235        log.debug("loaded manifest: task=%s root=%s samples=%d", task.value, root, len(samples))
236        return cls(samples=samples, task=task, root=root, batch_size=batch_size)
237
238    @classmethod
239    def from_hf(
240        cls,
241        hf_dataset: Any,
242        task: TaskType | str | None = None,
243        batch_size: int = 1,
244    ) -> Any:
245        """Wrap a ``datasets.Dataset`` as an iterable RPX source.
246
247        Returns a :class:`rpx_benchmark.data.hf_bridge.RPXHFBridge`
248        which is interchangeable with :class:`RPXDataset` at the
249        runner's consumer interface (``__len__`` + ``__iter__`` over
250        ``list[Sample]``).
251
252        Parameters
253        ----------
254        hf_dataset : datasets.Dataset
255            Dataset whose columns match
256            :func:`rpx_benchmark.data.features.features_for_task`.
257        task : TaskType or str, optional
258            Task this dataset serves. If omitted, we try to infer it
259            from ``hf_dataset.info.description`` (where
260            :func:`rpx_benchmark.data.load_hf` stamps it).
261        batch_size : int, default 1
262
263        Returns
264        -------
265        RPXHFBridge
266
267        Raises
268        ------
269        ManifestError
270            If ``task`` is neither provided nor discoverable on the
271            dataset metadata.
272        """
273        from .data.hf_bridge import RPXHFBridge  # noqa: PLC0415 — lazy
274
275        if task is None:
276            info = getattr(hf_dataset, "info", None)
277            config = getattr(info, "config_name", None) if info is not None else None
278            if config is None:
279                raise ManifestError(
280                    "Could not infer task from the HF dataset; pass task=TaskType.* explicitly.",
281                )
282            task = TaskType(config)
283        if isinstance(task, str):
284            task = TaskType(task)
285        return RPXHFBridge(hf_dataset=hf_dataset, task=task, batch_size=batch_size)
286
287    def __len__(self) -> int:
288        return len(self.samples)
289
290    def __iter__(self) -> Iterator[List[Sample]]:
291        batch: List[Sample] = []
292        for entry in self.samples:
293            batch.append(self._load_sample(entry))
294            if len(batch) == self.batch_size:
295                yield batch
296                batch = []
297        if batch:
298            yield batch
299
300    def _load_sample(self, entry: Dict[str, Any]) -> Sample:
301        rgb = self._load_rgb(entry["rgb"])
302
303        # Phase — from string label or integer session index
304        phase: Phase | None = None
305        if "phase" in entry:
306            v = entry["phase"]
307            phase = Phase(v) if isinstance(v, str) else _PHASE_INDEX.get(int(v))
308
309        # Difficulty
310        difficulty: Difficulty | None = None
311        if "difficulty" in entry:
312            difficulty = Difficulty(entry["difficulty"])
313
314        # Camera pose (T265 NPZ → 4×4 SE(3) float64)
315        camera_pose: np.ndarray | None = None
316        if "pose" in entry:
317            camera_pose = self._load_pose(entry["pose"])
318
319        # Fisheye images — stored in metadata if present
320        metadata: Dict[str, Any] = dict(entry.get("metadata") or {})
321        scene = entry.get("scene_id") or entry.get("scene")
322        if scene is not None:
323            metadata["scene_id"] = scene
324        if "fisheye_left" in entry:
325            metadata["fisheye_left"] = self._load_gray(entry["fisheye_left"])
326        if "fisheye_right" in entry:
327            metadata["fisheye_right"] = self._load_gray(entry["fisheye_right"])
328        # Second RGB frame for pair tasks (relative pose, keypoint matching)
329        if "rgb_b" in entry:
330            metadata["rgb_b"] = self._load_rgb(entry["rgb_b"])
331
332        gt = self._load_ground_truth(entry)
333
334        return Sample(
335            id=entry["id"],
336            rgb=rgb,
337            ground_truth=gt,
338            metadata=metadata or None,
339            phase=phase,
340            difficulty=difficulty,
341            camera_pose=camera_pose,
342        )
343
344    def _load_ground_truth(self, entry: Dict[str, Any]) -> Any:
345        task = self.task
346
347        if task == TaskType.MONOCULAR_DEPTH:
348            return DepthGroundTruth(depth_map=self._load_depth(entry["depth"]))
349
350        if task in (TaskType.OBJECT_DETECTION, TaskType.OPEN_VOCAB_DETECTION):
351            boxes, labels = self._load_boxes(entry["boxes"])
352            return DetectionGroundTruth(boxes=boxes, labels=labels)
353
354        if task == TaskType.OBJECT_SEGMENTATION:
355            return SegmentationGroundTruth(mask=self._load_mask(entry["mask"]))
356
357        if task == TaskType.OBJECT_TRACKING:
358            if "tracks" in entry and self._resolve(entry["tracks"]).is_file():
359                return self._load_tracklets(entry)
360            return self._load_tracking_mask(entry)
361
362        if task == TaskType.RELATIVE_CAMERA_POSE:
363            return self._load_relative_pose(entry)
364
365        if task == TaskType.VISUAL_GROUNDING:
366            return self._load_visual_grounding(entry)
367
368        if task == TaskType.SPARSE_DEPTH:
369            return self._load_sparse_depth(entry)
370
371
372        if task == TaskType.KEYPOINT_MATCHING:
373            return self._load_keypoints(entry)
374
375        raise ManifestError(
376            f"Unsupported task {task} — loader does not know how to parse "
377            "its ground-truth entries.",
378        )
379
380    # ------------------------------------------------------------------ #
381    # Ground-truth loaders
382    # ------------------------------------------------------------------ #
383
384    def _load_tracklets(self, entry: Dict[str, Any]) -> TrackletGroundTruth:
385        path = self._resolve(entry["tracks"])
386        with path.open("r", encoding="utf-8") as f:
387            data = json.load(f)
388        tracks = []
389        for item in data:
390            boxes = np.array(item["boxes"], dtype=np.float32)
391            scores = np.array(item["scores"], dtype=np.float32) if "scores" in item else None
392            tracks.append(Tracklet(track_id=str(item["track_id"]), boxes=boxes, scores=scores))
393        return TrackletGroundTruth(tracks=tracks)
394
395    def _load_tracking_mask(self, entry: Dict[str, Any]) -> TrackletGroundTruth:
396        """Represent one released instance-mask frame as one-step tracklets.
397
398        The D3 production runner groups the masks into complete clips before
399        evaluation.  This fallback keeps the generic frame loader usable for
400        inspection without depending on the unpublished ``tracklets/v1.json``.
401        """
402        mask = self._load_mask(entry["mask"])
403        tracks = []
404        for track_id in np.unique(mask):
405            if int(track_id) <= 0:
406                continue
407            ys, xs = np.nonzero(mask == track_id)
408            if xs.size == 0:
409                continue
410            boxes = np.array(
411                [[xs.min(), ys.min(), xs.max() + 1, ys.max() + 1]],
412                dtype=np.float32,
413            )
414            tracks.append(Tracklet(track_id=str(int(track_id)), boxes=boxes))
415        return TrackletGroundTruth(tracks=tracks)
416
417    def _load_relative_pose(self, entry: Dict[str, Any]) -> RelativePoseGroundTruth:
418        from .pose_conventions import relative_pose_from_raw_t265
419
420        # Published RPX poses retain librealsense's T265 axes. RCPE models
421        # consume D435 RGB and conventionally emit OpenCV camera coordinates,
422        # so convert the fixed axis basis before forming the relative pose.
423        pose_a = self._load_pose(entry["pose_a"])  # 4×4 SE(3)
424        pose_b = self._load_pose(entry["pose_b"])
425        T_rel = relative_pose_from_raw_t265(pose_a, pose_b)
426        return RelativePoseGroundTruth(
427            rotation=T_rel[:3, :3],
428            translation=T_rel[:3, 3],
429        )
430
431    def _load_visual_grounding(self, entry: Dict[str, Any]) -> VisualGroundingGroundTruth:
432        text = entry["text"]
433        boxes_raw = entry.get("boxes")
434        labels = entry.get("labels")
435        if boxes_raw is None:
436            boxes = np.zeros((0, 4), dtype=np.float32)
437        elif isinstance(boxes_raw, str):
438            gt_boxes, _ = self._load_boxes(boxes_raw)
439            boxes = gt_boxes
440        else:
441            boxes = np.array(boxes_raw, dtype=np.float32)
442        return VisualGroundingGroundTruth(text=text, boxes=boxes, labels=labels)
443
444    def _load_sparse_depth(self, entry: Dict[str, Any]) -> SparseDepthGroundTruth:
445        coordinates = self._load_array_or_inline(entry["coordinates"]).astype(np.float32)
446        depths = self._load_array_or_inline(entry["depths"]).astype(np.float32)
447        return SparseDepthGroundTruth(coordinates=coordinates, depths=depths)
448
449
450    def _load_keypoints(self, entry: Dict[str, Any]) -> KeypointCorrespondenceGroundTruth:
451        points0 = self._load_array_or_inline(entry["points0"]).astype(np.float32)
452        points1 = self._load_array_or_inline(entry["points1"]).astype(np.float32)
453        visibility = None
454        if "visibility" in entry:
455            visibility = self._load_array_or_inline(entry["visibility"]).astype(bool)
456        return KeypointCorrespondenceGroundTruth(
457            points0=points0, points1=points1, visibility=visibility
458        )
459
460    # ------------------------------------------------------------------ #
461    # Low-level file loaders
462    # ------------------------------------------------------------------ #
463
464    def _resolve(self, relative_path: str | Path) -> Path:
465        path = Path(relative_path)
466        return path if path.is_absolute() else self.root / path
467
468    def _load_rgb(self, relative_path: str) -> np.ndarray:
469        """Load an RGB image → ``(H, W, 3) uint8``.
470
471        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_rgb`
472        so the dtype/shape contract is enforced in exactly one place,
473        shared with every adapter that decodes directly.
474        """
475        from .decode_contracts import safe_load_rgb
476
477        return safe_load_rgb(self._resolve(relative_path))
478
479    def _load_depth(self, relative_path: str) -> np.ndarray:
480        """Load a 16-bit PNG depth file → ``(H, W) float32`` in metres.
481
482        Decoding is delegated to
483        :func:`rpx_benchmark.decode_contracts.safe_load_depth`, which
484        guarantees ``uint16`` (the only correct decode for 16-bit
485        depth PNGs). We then cast to float32 and scale mm → m.
486        """
487        from .decode_contracts import safe_load_depth
488
489        raw = safe_load_depth(self._resolve(relative_path))  # (H, W) uint16
490        depth_mm = raw.astype(np.float32)
491        depth_m = depth_mm / 1000.0
492        depth_m[depth_mm == 0] = 0.0  # keep 0 as "invalid" sentinel
493        return depth_m
494
495    def _load_pose(self, relative_path: str) -> np.ndarray:
496        """Load a T265 pose → 4×4 SE(3) float64 (camera-to-world).
497
498        Two on-disk formats are accepted:
499
500        * Legacy ``.npz`` (per-frame zip): ``position`` (3,) +
501          ``orientation`` (4, xyzw quaternion).
502        * Compact ``.npy`` (per-frame raw, produced by the
503          ``lossless-convert`` step): a single ``(7,) float64`` vector
504          packing ``[x, y, z, qx, qy, qz, qw]``. The values are
505          bit-identical to the legacy ``.npz`` form.
506        """
507        path = self._resolve(relative_path)
508        data = np.load(path)
509        if path.suffix.lower() == ".npy":
510            arr = np.asarray(data, dtype=np.float64)
511            if arr.shape != (7,):
512                raise ManifestError(
513                    f"Pose .npy at {path} must be shape (7,) float64; got {arr.shape}",
514                    hint="The dataset_hub lossless-convert step writes a "
515                    "single (7,) vector packing [x, y, z, qx, qy, qz, qw].",
516                )
517            position = arr[:3]
518            quat_xyzw = arr[3:]
519        else:
520            position = data["position"].astype(np.float64)  # (3,)
521            quat_xyzw = data["orientation"].astype(np.float64)  # (4,) xyzw
522
523        R = _quat_xyzw_to_rotmat(quat_xyzw)
524        T = np.eye(4, dtype=np.float64)
525        T[:3, :3] = R
526        T[:3, 3] = position
527        if T.shape != (4, 4) or T.dtype != np.float64:
528            raise ManifestError(
529                f"Pose load contract violated at {path}: "
530                f"expected (4, 4) float64, got shape {T.shape} dtype {T.dtype}",
531            )
532        return T
533
534    def _load_mask(self, relative_path: str) -> np.ndarray:
535        """Load an instance-segmentation mask → ``(H, W) int32``.
536
537        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_mask`.
538        """
539        from .decode_contracts import safe_load_mask
540
541        return safe_load_mask(self._resolve(relative_path))
542
543    def _load_gray(self, relative_path: str) -> np.ndarray:
544        """Load a grayscale image (fisheye) → ``(H, W) uint8``.
545
546        Delegates to :func:`rpx_benchmark.decode_contracts.safe_load_gray`.
547        """
548        from .decode_contracts import safe_load_gray
549
550        return safe_load_gray(self._resolve(relative_path))
551
552    def _load_boxes(self, relative_path: str):
553        path = self._resolve(relative_path)
554        with path.open("r", encoding="utf-8") as f:
555            data = json.load(f)
556        boxes = np.array([item["bbox"] for item in data], dtype=np.float32)
557        labels = [item["label"] for item in data]
558        return boxes, labels
559
560    def _load_array_or_inline(self, value: Any) -> np.ndarray:
561        if isinstance(value, str):
562            path = self._resolve(value)
563            if path.suffix.lower() == ".npy":
564                return np.load(path)
565            with path.open("r", encoding="utf-8") as f:
566                return np.array(json.load(f))
567        return np.array(value)

Iterates over RPX samples for a specific task.

Manifest format (JSON)::

{
  "task": "object_segmentation",
  "root": "/path/to/data",
  "samples": [
    {
      "id": "scene_001_clutter_00000",
      "scene": "scene_001",
      "phase": "clutter",
      "difficulty": "hard",
      "rgb":   "scene_001/0/rgb/00000.png",
      "depth": "scene_001/0/depth/00000.png",
      "mask":  "scene_001/0/mask/00000.png",
      "pose":  "scene_001/0/pose/00000.npz",
      ...
    }
  ]
}

All paths are relative to root. depth files are 16-bit PNG in millimetres (as saved by save_device_data.py). pose files are NPZ with keys position ([x,y,z] metres) and orientation ([x,y,z,w] quaternion) from the T265 tracker.

RPXDataset( samples: List[Dict[str, Any]], task: rpx_benchmark.api.TaskType, root: pathlib.Path, batch_size: int = 1)
samples: List[Dict[str, Any]]
root: pathlib.Path
batch_size: int = 1
@classmethod
def from_manifest( cls, manifest_path: str | pathlib.Path, batch_size: int = 1, validate: bool = False, max_samples: int | None = None) -> RPXDataset:
118    @classmethod
119    def from_manifest(
120        cls,
121        manifest_path: str | Path,
122        batch_size: int = 1,
123        validate: bool = False,
124        max_samples: int | None = None,
125    ) -> "RPXDataset":
126        """Load a manifest JSON file from disk and return a dataset.
127
128        Parameters
129        ----------
130        manifest_path : str or Path
131            Path to the manifest JSON. Produced either by
132            :func:`rpx_benchmark.hub.download_split` or by a custom
133            upload script.
134        batch_size : int
135            Number of samples per iteration. Default 1.
136        validate : bool, optional
137            When ``True``, validate the manifest against the strict
138            Pydantic schema in :mod:`rpx_benchmark.schemas` before
139            constructing the dataset. Requires the ``schemas`` extra
140            (``pip install 'rpx-benchmark[schemas]'``). Default
141            ``False`` preserves historical, tolerant behaviour.
142        max_samples : int, optional
143            Keep only the first N manifest samples. Intended for smoke
144            validation; must be at least 1 when provided.
145
146        Returns
147        -------
148        RPXDataset
149
150        Raises
151        ------
152        ManifestError
153            If the manifest file is missing, not valid JSON, is missing
154            required top-level fields, or (when ``validate=True``)
155            fails strict schema validation.
156        """
157        manifest_path = Path(manifest_path)
158        if not manifest_path.is_file():
159            raise ManifestError(
160                f"Manifest file not found: {manifest_path}",
161                hint="Did the HuggingFace download fail? Try rerunning with "
162                "--cache-dir pointing at a writable location.",
163            )
164        try:
165            with manifest_path.open("r", encoding="utf-8") as f:
166                manifest = json.load(f)
167        except json.JSONDecodeError as e:
168            raise ManifestError(
169                f"Manifest at {manifest_path} is not valid JSON: {e}",
170            ) from e
171        dataset = cls.from_dict(
172            manifest,
173            batch_size=batch_size,
174            default_root=manifest_path.parent,
175            validate=validate,
176        )
177        if max_samples is not None:
178            if max_samples < 1:
179                raise ManifestError(
180                    f"max_samples must be >= 1, got {max_samples}",
181                )
182            dataset.samples = dataset.samples[:max_samples]
183        return dataset

Load a manifest JSON file from disk and return a dataset.

Parameters

manifest_path : str or Path Path to the manifest JSON. Produced either by rpx_benchmark.hub.download_split() or by a custom upload script. batch_size : int Number of samples per iteration. Default 1. validate : bool, optional When True, validate the manifest against the strict Pydantic schema in rpx_benchmark.schemas before constructing the dataset. Requires the schemas extra (pip install 'rpx-benchmark[schemas]'). Default False preserves historical, tolerant behaviour. max_samples : int, optional Keep only the first N manifest samples. Intended for smoke validation; must be at least 1 when provided.

Returns

RPXDataset

Raises

ManifestError If the manifest file is missing, not valid JSON, is missing required top-level fields, or (when validate=True) fails strict schema validation.

@classmethod
def from_dict( cls, manifest: Dict[str, Any], batch_size: int = 1, default_root: str | pathlib.Path | None = None, validate: bool = False) -> RPXDataset:
185    @classmethod
186    def from_dict(
187        cls,
188        manifest: Dict[str, Any],
189        batch_size: int = 1,
190        default_root: str | Path | None = None,
191        validate: bool = False,
192    ) -> "RPXDataset":
193        """Build a dataset from an already-parsed manifest dict.
194
195        Raises
196        ------
197        ManifestError
198            If ``task`` is missing or unknown, or if ``samples`` is
199            missing. When ``validate=True``, any Pydantic validation
200            error is also surfaced as :class:`ManifestError`.
201        """
202        if validate:
203            # Opt-in strict validation via rpx_benchmark.schemas. Kept
204            # lazy so users who don't install the ``schemas`` extra
205            # don't hit pydantic import at module load time.
206            from .schemas import validate_manifest  # noqa: PLC0415 — lazy import
207
208            model = validate_manifest(manifest)
209            manifest = model.model_dump(exclude_none=True)
210
211        if "task" not in manifest:
212            raise ManifestError(
213                "Manifest is missing required field 'task'.",
214                hint="Task must be one of: " + ", ".join(t.value for t in TaskType),
215            )
216        try:
217            task = TaskType(manifest["task"])
218        except ValueError as e:
219            raise ManifestError(
220                f"Manifest task {manifest['task']!r} is not a known TaskType.",
221                hint="Expected one of: " + ", ".join(t.value for t in TaskType),
222            ) from e
223
224        if "samples" not in manifest:
225            raise ManifestError(
226                "Manifest is missing required field 'samples'.",
227            )
228
229        root = Path(manifest.get("root") or default_root or ".")
230        samples = manifest["samples"]
231        if not isinstance(samples, list):
232            raise ManifestError(
233                f"Manifest 'samples' must be a list, got {type(samples).__name__}",
234            )
235        log.debug("loaded manifest: task=%s root=%s samples=%d", task.value, root, len(samples))
236        return cls(samples=samples, task=task, root=root, batch_size=batch_size)

Build a dataset from an already-parsed manifest dict.

Raises

ManifestError If task is missing or unknown, or if samples is missing. When validate=True, any Pydantic validation error is also surfaced as ManifestError.

@classmethod
def from_hf( cls, hf_dataset: Any, task: rpx_benchmark.api.TaskType | str | None = None, batch_size: int = 1) -> Any:
238    @classmethod
239    def from_hf(
240        cls,
241        hf_dataset: Any,
242        task: TaskType | str | None = None,
243        batch_size: int = 1,
244    ) -> Any:
245        """Wrap a ``datasets.Dataset`` as an iterable RPX source.
246
247        Returns a :class:`rpx_benchmark.data.hf_bridge.RPXHFBridge`
248        which is interchangeable with :class:`RPXDataset` at the
249        runner's consumer interface (``__len__`` + ``__iter__`` over
250        ``list[Sample]``).
251
252        Parameters
253        ----------
254        hf_dataset : datasets.Dataset
255            Dataset whose columns match
256            :func:`rpx_benchmark.data.features.features_for_task`.
257        task : TaskType or str, optional
258            Task this dataset serves. If omitted, we try to infer it
259            from ``hf_dataset.info.description`` (where
260            :func:`rpx_benchmark.data.load_hf` stamps it).
261        batch_size : int, default 1
262
263        Returns
264        -------
265        RPXHFBridge
266
267        Raises
268        ------
269        ManifestError
270            If ``task`` is neither provided nor discoverable on the
271            dataset metadata.
272        """
273        from .data.hf_bridge import RPXHFBridge  # noqa: PLC0415 — lazy
274
275        if task is None:
276            info = getattr(hf_dataset, "info", None)
277            config = getattr(info, "config_name", None) if info is not None else None
278            if config is None:
279                raise ManifestError(
280                    "Could not infer task from the HF dataset; pass task=TaskType.* explicitly.",
281                )
282            task = TaskType(config)
283        if isinstance(task, str):
284            task = TaskType(task)
285        return RPXHFBridge(hf_dataset=hf_dataset, task=task, batch_size=batch_size)

Wrap a datasets.Dataset as an iterable RPX source.

Returns a rpx_benchmark.data.hf_bridge.RPXHFBridge which is interchangeable with RPXDataset at the runner's consumer interface (__len__ + __iter__ over list[Sample]).

Parameters

hf_dataset : datasets.Dataset Dataset whose columns match rpx_benchmark.data.features.features_for_task(). task : TaskType or str, optional Task this dataset serves. If omitted, we try to infer it from hf_dataset.info.description (where rpx_benchmark.data.load_hf() stamps it). batch_size : int, default 1

Returns

RPXHFBridge

Raises

ManifestError If task is neither provided nor discoverable on the dataset metadata.