Source code for tabench.models.adapters.callable_adapter

"""Black-box wrapper for arbitrary Python callables (P4).

Wraps anything that maps a scenario to link flows — a trained GNN, a
surrogate, a heuristic — into the standard model contract. The harness then
certifies its output exactly as it does for white-box solvers (P1) and
enforces the training-lineage fairness gate (P7).
"""

from __future__ import annotations

import time
from collections.abc import Callable

import numpy as np

from ...core.budget import Budget, BudgetCoords
from ...core.capabilities import Capabilities
from ...core.results import ResultBundle, Trace
from ...core.rng import RngBundle
from ...core.scenario import Scenario
from ..base import TrafficAssignmentModel

__all__ = ["CallableModel"]

FlowFn = Callable[[Scenario, np.random.Generator], np.ndarray]


[docs] class CallableModel(TrafficAssignmentModel): """Adapter turning ``fn(scenario, rng) -> link_flows`` into a benchmark model. ``capabilities`` are instance-level here (unlike library solvers, which declare them on the class), because they describe the wrapped artifact: pass ``trained_on`` lineage for learned models so the fairness gate can act on it. """ name = "callable" def __init__( self, fn: FlowFn, name: str = "callable", paradigm: str = "learned", deterministic: bool = False, seedable: bool = True, trained_on: tuple[str, ...] = (), ) -> None: super().__init__() self._fn = fn self.name = name self.capabilities = Capabilities( paradigm=paradigm, deterministic=deterministic, provides_gap=False, seedable=seedable, trained_on=trained_on, )
[docs] def solve( self, scenario: Scenario, budget: Budget, rng: RngBundle, trace: Trace ) -> ResultBundle: start = time.perf_counter() generator = rng.generator(source=0) flows = np.asarray(self._fn(scenario, generator), dtype=np.float64) if flows.shape != (scenario.network.n_links,): raise ValueError( f"Wrapped callable returned shape {flows.shape}, expected " f"({scenario.network.n_links},)" ) coords = BudgetCoords( iterations=1, sp_calls=0, wall_ms=1000.0 * (time.perf_counter() - start) ) trace.record(flows, coords) return ResultBundle( model_name=self.name, final=trace.final, trace=trace, factors={}, seed_info=rng.describe(), )