rpx_benchmark.examples.benchmark_tasks

Run six paper task examples from an installed wheel.

--smoke creates tiny synthetic fixtures and runs the real public evaluators. It needs no dataset, weights, GPU, or network.

For a real run, name the task: its data is downloaded first (only the files it needs), then scored. --model module:function plugs in your model; without it the matching function in rpx_benchmark.examples.dummy_models is scored, so every paper task runs before you write code. --manifest scores an existing manifest instead of downloading. T5/T6 use the canonical VQA JSONL contract, including the verified reference crop for T6; without --manifest they use the packaged, revision-pinned questions in rpx_benchmark/vqa/quickstart_v1.

  1"""Run six paper task examples from an installed wheel.
  2
  3``--smoke`` creates tiny synthetic fixtures and runs the real public evaluators.
  4It needs no dataset, weights, GPU, or network.
  5
  6For a real run, name the task: its data is downloaded first (only the files it
  7needs), then scored. ``--model module:function`` plugs in your model; without it
  8the matching function in ``rpx_benchmark.examples.dummy_models`` is scored, so
  9every paper task runs before you write code. ``--manifest`` scores an existing
 10manifest instead of downloading. T5/T6 use the canonical VQA JSONL contract,
 11including the verified reference crop for T6; without ``--manifest`` they use
 12the packaged, revision-pinned questions in ``rpx_benchmark/vqa/quickstart_v1``.
 13"""
 14
 15from __future__ import annotations
 16
 17import argparse
 18import hashlib
 19import io
 20import json
 21from pathlib import Path
 22from typing import Any, Callable
 23
 24import numpy as np
 25from PIL import Image
 26
 27import rpx_benchmark as rpx
 28
 29from ..exceptions import ConfigError
 30from ..quickstart import download
 31from ..tasks._pipeline import PipelineResult, TaskRunConfig
 32from ..vqa.contract import VQASample, image_locator, load_manifest, write_manifest
 33from ..vqa.evaluate import evaluate_vqa
 34from ..vqa.hub_rgb import image_cache_name, reference_cache_name
 35from . import dummy_models
 36from .profile_callable import load_callable
 37
 38TASKS: dict[
 39    str,
 40    tuple[
 41        Callable[..., rpx.BenchmarkableModel], type[TaskRunConfig], Callable[[Any], PipelineResult]
 42    ],
 43] = {
 44    "T1": (rpx.make_numpy_depth_model, rpx.MonocularDepthRunConfig, rpx.run_monocular_depth),
 45    "T2": (rpx.make_numpy_video_depth_model, rpx.VideoDepthRunConfig, rpx.run_video_depth),
 46    "T3": (rpx.make_numpy_tracking_model, rpx.ObjectTrackingRunConfig, rpx.run_object_tracking),
 47    "T4": (rpx.make_numpy_pose_model, rpx.RelativePoseRunConfig, rpx.run_relative_pose),
 48}
 49
 50
 51#: Hub manifest folder for each task's ``download_split`` call.
 52#: Packaged, revision-pinned VQA questions for T5/T6 runs without --manifest.
 53VQA_QUICKSTART = Path(__file__).resolve().parents[1] / "vqa" / "quickstart_v1"
 54
 55#: Dummy model scored for each task when no --model is given.
 56DUMMY_MODELS: dict[str, Callable[..., Any]] = {
 57    "T1": dummy_models.image_depth,
 58    "T2": dummy_models.video_depth,
 59    "T3": dummy_models.tracking,
 60    "T4": dummy_models.camera_pose,
 61    "T5": dummy_models.vqa,
 62    "T6": dummy_models.incontext_vqa,
 63}
 64
 65HUB_MANIFESTS = {
 66    "T1": ("monocular_depth", None),
 67    "T2": ("video_depth", None),
 68    "T3": ("object_tracking", None),
 69    "T4": ("relative_camera_pose", "relative_pose"),
 70}
 71
 72
 73def demo_depth(rgb):
 74    """Constant integration fixture; not pretrained inference."""
 75    return np.full(rgb.shape[:-1], 2.1, np.float32)
 76
 77
 78def demo_video_depth(rgb):
 79    """Non-degenerate depth ramp for the video validator's synthetic case."""
 80    return np.broadcast_to(np.linspace(1.9, 2.1, rgb.shape[2]), rgb.shape[:3]).astype(np.float32)
 81
 82
 83def demo_tracking(rgb):
 84    """One fixed track for synthetic data; real trackers need persistent state."""
 85    return [{"track_id": "1", "boxes": np.array([[3, 2, 12, 10]], np.float32)}]
 86
 87
 88def demo_pose(rgb_a, rgb_b):
 89    """Fixed 10-cm translation fixture; not a camera pose estimator."""
 90    return {"rotation": np.eye(3), "translation": np.array([0.1, 0.0, 0.0])}
 91
 92
 93def demo_vqa(image_paths, prompt, max_new_tokens, output_kind):
 94    """Fixed synthetic localization; never use this as a benchmark model."""
 95    return '{"label":"cup","bbox":[0,0,1000,1000]}'
 96
 97
 98def create_smoke(root: Path, task: str) -> tuple[Path, Callable[..., Any]]:
 99    """Create clearly synthetic local inputs with no released annotation edits."""
100    root.mkdir(parents=True, exist_ok=True)
101    rgb = np.full((16, 20, 3), 128, np.uint8)
102    Image.fromarray(rgb).save(root / "rgb.png")
103    if task in {"T5", "T6"}:
104        image = image_locator({"scene_id": "scene012", "kind": "mos", "phase": 1, "frame": "00113"})
105        row = dict(
106            sample_id=f"synthetic-{task}",
107            scene_id="scene012",
108            kind="mos",
109            phase=1,
110            frame="00113",
111            img_w=20,
112            img_h=16,
113            image=image,
114            question_type="attr_single_color",
115            question="Locate the gray cup.",
116            answer="cup",
117            answer_bbox=[0, 0, 19, 15],
118        )
119        if task == "T6":
120            buffer = io.BytesIO()
121            Image.new("RGB", (4, 4), "gray").save(buffer, format="PNG")
122            crop = buffer.getvalue()
123            row.update(
124                question_type="inctx_attr_single_color",
125                reference_image={**image, "shard": "synthetic/reference/rgb.tar"},
126                reference_crop_bbox=[0, 0, 3, 3],
127                reference_crop_sha256=hashlib.sha256(crop).hexdigest(),
128                source_sample_id="synthetic-T5",
129            )
130        vqa_sample = VQASample.from_dict(row)
131        cache = root / "images"
132        cache.mkdir()
133        # Populate the normal cache with SYNTHETIC pixels, only under this
134        # isolated fixture root. Real runs fetch the manifest's actual images.
135        Image.fromarray(rgb).save(cache / image_cache_name(vqa_sample))
136        if task == "T6":
137            (cache / reference_cache_name(vqa_sample)).write_bytes(crop)
138        manifest = root / "manifest.jsonl"
139        write_manifest([vqa_sample], manifest)
140        return manifest, demo_vqa
141    Image.fromarray(np.full((16, 20), 2000, np.uint16)).save(root / "depth.png")
142    mask = np.zeros((16, 20), np.uint8)
143    mask[2:10, 3:12] = 1
144    Image.fromarray(mask).save(root / "mask.png")
145    np.savez(root / "pose.npz", position=np.zeros(3), orientation=np.array([0.0, 0.0, 0.0, 1.0]))
146    sample: dict[str, Any] = dict(
147        id=f"synthetic-{task}",
148        scene="scene012",
149        scene_id="scene012",
150        phase="interaction",
151        difficulty="easy",
152        rgb="rgb.png",
153    )
154    task_names = {
155        "T1": "monocular_depth",
156        "T2": "video_depth",
157        "T3": "object_tracking",
158        "T4": "relative_camera_pose",
159    }
160    fn: Callable[..., Any]
161    if task == "T1":
162        sample["depth"] = "depth.png"
163        fn = demo_depth
164    elif task == "T2":
165        sample.update(phase=1, frame_filenames=["rgb.png"] * 3, depth_filenames=["depth.png"] * 3)
166        fn = demo_video_depth
167    elif task == "T3":
168        sample["mask"] = "mask.png"
169        fn = demo_tracking
170    else:
171        np.savez(
172            root / "pose_b.npz",
173            position=np.array([0.1, 0.0, 0.0]),
174            orientation=np.array([0.0, 0.0, 0.0, 1.0]),
175        )
176        sample.update(rgb_b="rgb.png", pose_a="pose.npz", pose_b="pose_b.npz")
177        fn = demo_pose
178    manifest = root / "manifest.json"
179    manifest.write_text(
180        json.dumps(
181            {"task": task_names[task], "root": str(root.resolve()), "samples": [sample]}, indent=2
182        )
183        + "\n"
184    )
185    return manifest, fn
186
187
188def run_task(
189    task: str,
190    manifest: Path,
191    fn: Callable[..., Any],
192    output: Path,
193    *,
194    device: str = "cpu",
195    split: str = "hard",
196    max_samples: int | None = None,
197    image_cache: Path | None = None,
198    model_name: str = "custom",
199    model_revision: str = "unspecified",
200    depth_output_kind: str = "metric",
201) -> dict[str, Any]:
202    """Run one public task protocol with a supplied callable and manifest."""
203    if output.exists() and any(output.iterdir()):
204        raise ConfigError(f"Use a new, empty output directory: {output}")
205    if task in {"T5", "T6"}:
206        samples = load_manifest(manifest)
207        if any(sample.is_in_context != (task == "T6") for sample in samples):
208            raise ConfigError(f"{task} manifest uses the wrong one-image/two-image protocol")
209        if max_samples is not None:
210            raise ConfigError("For T5/T6 select a subset manifest first; all its rows are scored")
211        return evaluate_vqa(
212            manifest,
213            fn,
214            model_name=model_name,
215            model_revision=model_revision,
216            output_dir=output,
217            image_cache=image_cache or output / "images",
218        )
219    wrap, config, runner = TASKS[task]
220    kwargs = {"depth_output_kind": depth_output_kind} if task in {"T1", "T2"} else {}
221    cfg = config(
222        model=wrap(fn, name=model_name, **kwargs),
223        manifest_path=str(manifest),
224        output_dir=str(output),
225        device=device,
226        split=split,
227        require_cuda=device.startswith("cuda"),
228        skip_flops=True,
229        max_samples=max_samples,
230    )
231    result, _, paths = runner(cfg)
232    config_path = output / "run_config.json"
233    config_path.write_text(
234        json.dumps(
235            {
236                "task": task,
237                "model": model_name,
238                "model_revision": model_revision,
239                "manifest_sha256": hashlib.sha256(manifest.read_bytes()).hexdigest(),
240                "device": device,
241                "split": split,
242                "max_samples": max_samples,
243                "depth_output_kind": depth_output_kind if task in {"T1", "T2"} else None,
244                "protocol": "public_callable_task_runner",
245                "skip_flops": True,
246            },
247            indent=2,
248        )
249        + "\n"
250    )
251    paths["run_config"] = config_path
252    return {
253        "task": task,
254        "num_samples": result.num_samples,
255        "aggregated": result.aggregated,
256        "paths": {key: str(path) for key, path in paths.items()},
257    }
258
259
260TASK_LABELS = {
261    "T1": "Image depth",
262    "T2": "Video depth",
263    "T3": "Object tracking",
264    "T4": "Relative camera pose",
265    "T5": "VQA",
266    "T6": "In-context VQA",
267}
268# Headline metrics shown in the console summary (all metrics are in result files).
269HEADLINE = {
270    "T1": (("absrel", "AbsRel"), ("delta1", "δ1")),
271    "T2": (("absrel", "AbsRel"), ("tgm", "TGM")),
272    "T3": (("mota", "MOTA"), ("idsw", "IDSW")),
273    "T4": (("rotation_error_deg", "Rot°"), ("translation_angular_deg", "Transl°")),
274    "T5": (("bbox_accuracy_at_0_5", "Acc@0.5"), ("bbox_mean_giou", "GIoU")),
275    "T6": (("bbox_accuracy_at_0_5", "Acc@0.5"), ("bbox_mean_giou", "GIoU")),
276}
277
278
279def summary_line(task: str, report: dict[str, Any], out_dir: Path) -> str:
280    """One readable console line: task, sample count, headline metrics, output folder."""
281    metrics = report.get("aggregated") or report.get("metrics") or {}
282    shown = []
283    for key, label in HEADLINE[task]:
284        value = metrics.get(key)
285        if isinstance(value, (int, float)):
286            shown.append(f"{label} {value:.3f}")
287    samples = report.get("num_samples", "?")
288    return f"  {task}  {TASK_LABELS[task]:<21}{samples:>5} samples   {' · '.join(shown):<34} → {out_dir}"
289
290
291def default_output(task: str, model: str | None) -> Path:
292    """``rpx_results/<task>/<model>``, numbered so repeated runs never collide."""
293    label = model.replace(":", "-").replace("/", "-") if model else "dummy"
294    base = Path("rpx_results") / task / label
295    candidate, n = base, 1
296    while candidate.exists() and any(candidate.iterdir()):
297        n += 1
298        candidate = base.with_name(f"{label}-{n}")
299    return candidate
300
301
302def main() -> None:
303    parser = argparse.ArgumentParser(description=__doc__)
304    parser.add_argument("--task", choices=[*TASKS, "T5", "T6", "all"], required=True)
305    parser.add_argument("--smoke", action="store_true")
306    parser.add_argument("--manifest", type=Path)
307    parser.add_argument("--model", help="module:function; the model is loaded by your module")
308    parser.add_argument("--model-name", default="custom")
309    parser.add_argument("--model-revision", default="unspecified")
310    parser.add_argument("--device", default="cpu")
311    parser.add_argument("--split", choices=["easy", "medium", "hard"], default="hard")
312    parser.add_argument("--max-samples", type=int)
313    parser.add_argument("--depth-output-kind", choices=["metric", "relative"], default="metric")
314    parser.add_argument("--image-cache", type=Path)
315    parser.add_argument(
316        "--full",
317        action="store_true",
318        help="T5/T6: score the paper's full question list instead of the packaged quick start",
319    )
320    parser.add_argument(
321        "--output",
322        type=Path,
323        help="results folder (default: rpx_results/<task>/<model>, numbered if taken)",
324    )
325    parser.add_argument(
326        "--json",
327        action="store_true",
328        help="print the full reports as JSON lines instead of a summary",
329    )
330    args = parser.parse_args()
331    if args.smoke:
332        args.output = args.output or Path("rpx-check")
333        if args.model or args.manifest or args.image_cache or args.device != "cpu":
334            parser.error(
335                "--smoke uses isolated synthetic fixtures and CPU; omit model/manifest/cache"
336            )
337        if args.output.exists() and any(args.output.iterdir()):
338            parser.error("Use a new, empty smoke output directory")
339        task_list = list(TASKS) + ["T5", "T6"] if args.task == "all" else [args.task]
340        if not args.json:
341            print("RPX smoke check: synthetic inputs, CPU, real evaluators")
342        for task in task_list:
343            root = args.output / task
344            manifest, fn = create_smoke(root / "synthetic_inputs", task)
345            report = run_task(
346                task,
347                manifest,
348                fn,
349                root / "results",
350                split="easy",
351                image_cache=root / "synthetic_inputs/images",
352                model_name="synthetic-integration-fixture",
353                model_revision="demo-v1",
354            )
355            if args.json:
356                print(json.dumps({"task": task, "synthetic": True, "report": report}, default=str))
357            else:
358                print(summary_line(task, report, root / "results"))
359        if not args.json:
360            print(
361                f"Done: {len(task_list)} task(s) ran end to end; full results under {args.output}/.\n"
362                "Next, score your own model: https://irvlutd.github.io/RPX/toolkit-docs/models/"
363            )
364    else:
365        if args.task == "all":
366            parser.error("Real runs score one task at a time: pass --task T1 ... T6")
367        if args.full and (args.task not in {"T5", "T6"} or args.manifest):
368            parser.error("--full selects the full T5/T6 question list; drop it, or drop --manifest")
369        output = args.output or default_output(args.task, args.model)
370        manifest = args.manifest
371        max_samples = args.max_samples
372        image_cache = args.image_cache
373        if manifest is None:
374            manifest = download(args.task, args.split, args.max_samples, full=args.full)
375            max_samples = None  # the downloaded manifest already holds exactly these rows
376            if args.task in {"T5", "T6"}:
377                image_cache = image_cache or manifest.parent / "vqa-images"
378        if args.model:
379            fn, model_name = load_callable(args.model), args.model_name
380        else:
381            fn, model_name = DUMMY_MODELS[args.task], "dummy"
382            print(
383                "No --model given: scoring the dummy model in rpx_benchmark.examples.dummy_models. "
384                "Pass --model my_model:predict to score yours."
385            )
386        print(f"Scoring {TASK_LABELS[args.task]} ...")
387        report = run_task(
388            args.task,
389            manifest,
390            fn,
391            output,
392            device=args.device,
393            split=args.split,
394            max_samples=max_samples,
395            image_cache=image_cache,
396            model_name=model_name,
397            model_revision=args.model_revision,
398            depth_output_kind=args.depth_output_kind,
399        )
400        if args.json:
401            print(json.dumps(report, default=str))
402        else:
403            print(summary_line(args.task, report, output))
404
405
406if __name__ == "__main__":
407    main()
TASKS: dict[str, tuple[typing.Callable[..., rpx_benchmark.adapters.BenchmarkableModel], type[rpx_benchmark.tasks.TaskRunConfig], typing.Callable[[typing.Any], typing.Tuple[rpx_benchmark.metrics.registry.BenchmarkResult, rpx_benchmark.deployment.DeploymentReadinessReport, typing.Dict[str, pathlib.Path]]]]] = {'T1': (<function make_numpy_depth_model>, <class 'rpx_benchmark.tasks.MonocularDepthRunConfig'>, <function run_monocular_depth>), 'T2': (<function make_numpy_video_depth_model>, <class 'rpx_benchmark.tasks.video_depth.VideoDepthRunConfig'>, <function run_video_depth>), 'T3': (<function make_numpy_tracking_model>, <class 'rpx_benchmark.tasks.ObjectTrackingRunConfig'>, <function run_object_tracking>), 'T4': (<function make_numpy_pose_model>, <class 'rpx_benchmark.tasks.RelativePoseRunConfig'>, <function run_relative_pose>)}
VQA_QUICKSTART = PosixPath('/home/runner/work/RPX/RPX/benchmark/rpx_benchmark/vqa/quickstart_v1')
DUMMY_MODELS: dict[str, typing.Callable[..., typing.Any]] = {'T1': <function image_depth>, 'T2': <function video_depth>, 'T3': <function tracking>, 'T4': <function camera_pose>, 'T5': <function vqa>, 'T6': <function incontext_vqa>}
HUB_MANIFESTS = {'T1': ('monocular_depth', None), 'T2': ('video_depth', None), 'T3': ('object_tracking', None), 'T4': ('relative_camera_pose', 'relative_pose')}
def demo_depth(rgb):
74def demo_depth(rgb):
75    """Constant integration fixture; not pretrained inference."""
76    return np.full(rgb.shape[:-1], 2.1, np.float32)

Constant integration fixture; not pretrained inference.

def demo_video_depth(rgb):
79def demo_video_depth(rgb):
80    """Non-degenerate depth ramp for the video validator's synthetic case."""
81    return np.broadcast_to(np.linspace(1.9, 2.1, rgb.shape[2]), rgb.shape[:3]).astype(np.float32)

Non-degenerate depth ramp for the video validator's synthetic case.

def demo_tracking(rgb):
84def demo_tracking(rgb):
85    """One fixed track for synthetic data; real trackers need persistent state."""
86    return [{"track_id": "1", "boxes": np.array([[3, 2, 12, 10]], np.float32)}]

One fixed track for synthetic data; real trackers need persistent state.

def demo_pose(rgb_a, rgb_b):
89def demo_pose(rgb_a, rgb_b):
90    """Fixed 10-cm translation fixture; not a camera pose estimator."""
91    return {"rotation": np.eye(3), "translation": np.array([0.1, 0.0, 0.0])}

Fixed 10-cm translation fixture; not a camera pose estimator.

def demo_vqa(image_paths, prompt, max_new_tokens, output_kind):
94def demo_vqa(image_paths, prompt, max_new_tokens, output_kind):
95    """Fixed synthetic localization; never use this as a benchmark model."""
96    return '{"label":"cup","bbox":[0,0,1000,1000]}'

Fixed synthetic localization; never use this as a benchmark model.

def create_smoke( root: pathlib.Path, task: str) -> tuple[pathlib.Path, typing.Callable[..., typing.Any]]:
 99def create_smoke(root: Path, task: str) -> tuple[Path, Callable[..., Any]]:
100    """Create clearly synthetic local inputs with no released annotation edits."""
101    root.mkdir(parents=True, exist_ok=True)
102    rgb = np.full((16, 20, 3), 128, np.uint8)
103    Image.fromarray(rgb).save(root / "rgb.png")
104    if task in {"T5", "T6"}:
105        image = image_locator({"scene_id": "scene012", "kind": "mos", "phase": 1, "frame": "00113"})
106        row = dict(
107            sample_id=f"synthetic-{task}",
108            scene_id="scene012",
109            kind="mos",
110            phase=1,
111            frame="00113",
112            img_w=20,
113            img_h=16,
114            image=image,
115            question_type="attr_single_color",
116            question="Locate the gray cup.",
117            answer="cup",
118            answer_bbox=[0, 0, 19, 15],
119        )
120        if task == "T6":
121            buffer = io.BytesIO()
122            Image.new("RGB", (4, 4), "gray").save(buffer, format="PNG")
123            crop = buffer.getvalue()
124            row.update(
125                question_type="inctx_attr_single_color",
126                reference_image={**image, "shard": "synthetic/reference/rgb.tar"},
127                reference_crop_bbox=[0, 0, 3, 3],
128                reference_crop_sha256=hashlib.sha256(crop).hexdigest(),
129                source_sample_id="synthetic-T5",
130            )
131        vqa_sample = VQASample.from_dict(row)
132        cache = root / "images"
133        cache.mkdir()
134        # Populate the normal cache with SYNTHETIC pixels, only under this
135        # isolated fixture root. Real runs fetch the manifest's actual images.
136        Image.fromarray(rgb).save(cache / image_cache_name(vqa_sample))
137        if task == "T6":
138            (cache / reference_cache_name(vqa_sample)).write_bytes(crop)
139        manifest = root / "manifest.jsonl"
140        write_manifest([vqa_sample], manifest)
141        return manifest, demo_vqa
142    Image.fromarray(np.full((16, 20), 2000, np.uint16)).save(root / "depth.png")
143    mask = np.zeros((16, 20), np.uint8)
144    mask[2:10, 3:12] = 1
145    Image.fromarray(mask).save(root / "mask.png")
146    np.savez(root / "pose.npz", position=np.zeros(3), orientation=np.array([0.0, 0.0, 0.0, 1.0]))
147    sample: dict[str, Any] = dict(
148        id=f"synthetic-{task}",
149        scene="scene012",
150        scene_id="scene012",
151        phase="interaction",
152        difficulty="easy",
153        rgb="rgb.png",
154    )
155    task_names = {
156        "T1": "monocular_depth",
157        "T2": "video_depth",
158        "T3": "object_tracking",
159        "T4": "relative_camera_pose",
160    }
161    fn: Callable[..., Any]
162    if task == "T1":
163        sample["depth"] = "depth.png"
164        fn = demo_depth
165    elif task == "T2":
166        sample.update(phase=1, frame_filenames=["rgb.png"] * 3, depth_filenames=["depth.png"] * 3)
167        fn = demo_video_depth
168    elif task == "T3":
169        sample["mask"] = "mask.png"
170        fn = demo_tracking
171    else:
172        np.savez(
173            root / "pose_b.npz",
174            position=np.array([0.1, 0.0, 0.0]),
175            orientation=np.array([0.0, 0.0, 0.0, 1.0]),
176        )
177        sample.update(rgb_b="rgb.png", pose_a="pose.npz", pose_b="pose_b.npz")
178        fn = demo_pose
179    manifest = root / "manifest.json"
180    manifest.write_text(
181        json.dumps(
182            {"task": task_names[task], "root": str(root.resolve()), "samples": [sample]}, indent=2
183        )
184        + "\n"
185    )
186    return manifest, fn

Create clearly synthetic local inputs with no released annotation edits.

def run_task( task: str, manifest: pathlib.Path, fn: Callable[..., Any], output: pathlib.Path, *, device: str = 'cpu', split: str = 'hard', max_samples: int | None = None, image_cache: pathlib.Path | None = None, model_name: str = 'custom', model_revision: str = 'unspecified', depth_output_kind: str = 'metric') -> dict[str, typing.Any]:
189def run_task(
190    task: str,
191    manifest: Path,
192    fn: Callable[..., Any],
193    output: Path,
194    *,
195    device: str = "cpu",
196    split: str = "hard",
197    max_samples: int | None = None,
198    image_cache: Path | None = None,
199    model_name: str = "custom",
200    model_revision: str = "unspecified",
201    depth_output_kind: str = "metric",
202) -> dict[str, Any]:
203    """Run one public task protocol with a supplied callable and manifest."""
204    if output.exists() and any(output.iterdir()):
205        raise ConfigError(f"Use a new, empty output directory: {output}")
206    if task in {"T5", "T6"}:
207        samples = load_manifest(manifest)
208        if any(sample.is_in_context != (task == "T6") for sample in samples):
209            raise ConfigError(f"{task} manifest uses the wrong one-image/two-image protocol")
210        if max_samples is not None:
211            raise ConfigError("For T5/T6 select a subset manifest first; all its rows are scored")
212        return evaluate_vqa(
213            manifest,
214            fn,
215            model_name=model_name,
216            model_revision=model_revision,
217            output_dir=output,
218            image_cache=image_cache or output / "images",
219        )
220    wrap, config, runner = TASKS[task]
221    kwargs = {"depth_output_kind": depth_output_kind} if task in {"T1", "T2"} else {}
222    cfg = config(
223        model=wrap(fn, name=model_name, **kwargs),
224        manifest_path=str(manifest),
225        output_dir=str(output),
226        device=device,
227        split=split,
228        require_cuda=device.startswith("cuda"),
229        skip_flops=True,
230        max_samples=max_samples,
231    )
232    result, _, paths = runner(cfg)
233    config_path = output / "run_config.json"
234    config_path.write_text(
235        json.dumps(
236            {
237                "task": task,
238                "model": model_name,
239                "model_revision": model_revision,
240                "manifest_sha256": hashlib.sha256(manifest.read_bytes()).hexdigest(),
241                "device": device,
242                "split": split,
243                "max_samples": max_samples,
244                "depth_output_kind": depth_output_kind if task in {"T1", "T2"} else None,
245                "protocol": "public_callable_task_runner",
246                "skip_flops": True,
247            },
248            indent=2,
249        )
250        + "\n"
251    )
252    paths["run_config"] = config_path
253    return {
254        "task": task,
255        "num_samples": result.num_samples,
256        "aggregated": result.aggregated,
257        "paths": {key: str(path) for key, path in paths.items()},
258    }

Run one public task protocol with a supplied callable and manifest.

TASK_LABELS = {'T1': 'Image depth', 'T2': 'Video depth', 'T3': 'Object tracking', 'T4': 'Relative camera pose', 'T5': 'VQA', 'T6': 'In-context VQA'}
HEADLINE = {'T1': (('absrel', 'AbsRel'), ('delta1', 'δ1')), 'T2': (('absrel', 'AbsRel'), ('tgm', 'TGM')), 'T3': (('mota', 'MOTA'), ('idsw', 'IDSW')), 'T4': (('rotation_error_deg', 'Rot°'), ('translation_angular_deg', 'Transl°')), 'T5': (('bbox_accuracy_at_0_5', 'Acc@0.5'), ('bbox_mean_giou', 'GIoU')), 'T6': (('bbox_accuracy_at_0_5', 'Acc@0.5'), ('bbox_mean_giou', 'GIoU'))}
def summary_line(task: str, report: dict[str, typing.Any], out_dir: pathlib.Path) -> str:
280def summary_line(task: str, report: dict[str, Any], out_dir: Path) -> str:
281    """One readable console line: task, sample count, headline metrics, output folder."""
282    metrics = report.get("aggregated") or report.get("metrics") or {}
283    shown = []
284    for key, label in HEADLINE[task]:
285        value = metrics.get(key)
286        if isinstance(value, (int, float)):
287            shown.append(f"{label} {value:.3f}")
288    samples = report.get("num_samples", "?")
289    return f"  {task}  {TASK_LABELS[task]:<21}{samples:>5} samples   {' · '.join(shown):<34} → {out_dir}"

One readable console line: task, sample count, headline metrics, output folder.

def default_output(task: str, model: str | None) -> pathlib.Path:
292def default_output(task: str, model: str | None) -> Path:
293    """``rpx_results/<task>/<model>``, numbered so repeated runs never collide."""
294    label = model.replace(":", "-").replace("/", "-") if model else "dummy"
295    base = Path("rpx_results") / task / label
296    candidate, n = base, 1
297    while candidate.exists() and any(candidate.iterdir()):
298        n += 1
299        candidate = base.with_name(f"{label}-{n}")
300    return candidate

rpx_results/<task>/<model>, numbered so repeated runs never collide.

def main() -> None:
303def main() -> None:
304    parser = argparse.ArgumentParser(description=__doc__)
305    parser.add_argument("--task", choices=[*TASKS, "T5", "T6", "all"], required=True)
306    parser.add_argument("--smoke", action="store_true")
307    parser.add_argument("--manifest", type=Path)
308    parser.add_argument("--model", help="module:function; the model is loaded by your module")
309    parser.add_argument("--model-name", default="custom")
310    parser.add_argument("--model-revision", default="unspecified")
311    parser.add_argument("--device", default="cpu")
312    parser.add_argument("--split", choices=["easy", "medium", "hard"], default="hard")
313    parser.add_argument("--max-samples", type=int)
314    parser.add_argument("--depth-output-kind", choices=["metric", "relative"], default="metric")
315    parser.add_argument("--image-cache", type=Path)
316    parser.add_argument(
317        "--full",
318        action="store_true",
319        help="T5/T6: score the paper's full question list instead of the packaged quick start",
320    )
321    parser.add_argument(
322        "--output",
323        type=Path,
324        help="results folder (default: rpx_results/<task>/<model>, numbered if taken)",
325    )
326    parser.add_argument(
327        "--json",
328        action="store_true",
329        help="print the full reports as JSON lines instead of a summary",
330    )
331    args = parser.parse_args()
332    if args.smoke:
333        args.output = args.output or Path("rpx-check")
334        if args.model or args.manifest or args.image_cache or args.device != "cpu":
335            parser.error(
336                "--smoke uses isolated synthetic fixtures and CPU; omit model/manifest/cache"
337            )
338        if args.output.exists() and any(args.output.iterdir()):
339            parser.error("Use a new, empty smoke output directory")
340        task_list = list(TASKS) + ["T5", "T6"] if args.task == "all" else [args.task]
341        if not args.json:
342            print("RPX smoke check: synthetic inputs, CPU, real evaluators")
343        for task in task_list:
344            root = args.output / task
345            manifest, fn = create_smoke(root / "synthetic_inputs", task)
346            report = run_task(
347                task,
348                manifest,
349                fn,
350                root / "results",
351                split="easy",
352                image_cache=root / "synthetic_inputs/images",
353                model_name="synthetic-integration-fixture",
354                model_revision="demo-v1",
355            )
356            if args.json:
357                print(json.dumps({"task": task, "synthetic": True, "report": report}, default=str))
358            else:
359                print(summary_line(task, report, root / "results"))
360        if not args.json:
361            print(
362                f"Done: {len(task_list)} task(s) ran end to end; full results under {args.output}/.\n"
363                "Next, score your own model: https://irvlutd.github.io/RPX/toolkit-docs/models/"
364            )
365    else:
366        if args.task == "all":
367            parser.error("Real runs score one task at a time: pass --task T1 ... T6")
368        if args.full and (args.task not in {"T5", "T6"} or args.manifest):
369            parser.error("--full selects the full T5/T6 question list; drop it, or drop --manifest")
370        output = args.output or default_output(args.task, args.model)
371        manifest = args.manifest
372        max_samples = args.max_samples
373        image_cache = args.image_cache
374        if manifest is None:
375            manifest = download(args.task, args.split, args.max_samples, full=args.full)
376            max_samples = None  # the downloaded manifest already holds exactly these rows
377            if args.task in {"T5", "T6"}:
378                image_cache = image_cache or manifest.parent / "vqa-images"
379        if args.model:
380            fn, model_name = load_callable(args.model), args.model_name
381        else:
382            fn, model_name = DUMMY_MODELS[args.task], "dummy"
383            print(
384                "No --model given: scoring the dummy model in rpx_benchmark.examples.dummy_models. "
385                "Pass --model my_model:predict to score yours."
386            )
387        print(f"Scoring {TASK_LABELS[args.task]} ...")
388        report = run_task(
389            args.task,
390            manifest,
391            fn,
392            output,
393            device=args.device,
394            split=args.split,
395            max_samples=max_samples,
396            image_cache=image_cache,
397            model_name=model_name,
398            model_revision=args.model_revision,
399            depth_output_kind=args.depth_output_kind,
400        )
401        if args.json:
402            print(json.dumps(report, default=str))
403        else:
404            print(summary_line(args.task, report, output))