Source code for tabench.models.base

"""The model contract: one abstract method plus a capabilities declaration (P4).

A white-box model is one whose internals match the scenario's declared cost
functions (``Network.link_cost``); the harness certifies *any* model's output
through those functions regardless (P1), so white-box status affects what the
model may exploit, not how it is scored.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, ClassVar

from ..core.budget import Budget
from ..core.capabilities import Capabilities
from ..core.factors import FactorSpec, resolve_factors
from ..core.results import ResultBundle, Trace
from ..core.rng import RngBundle
from ..core.scenario import Scenario

__all__ = ["TrafficAssignmentModel", "MODEL_REGISTRY", "register_model"]


[docs] class TrafficAssignmentModel(ABC): """Base class every benchmark model or wrapper implements.""" name: ClassVar[str] = "unnamed" capabilities: ClassVar[Capabilities] factors: ClassVar[dict[str, FactorSpec]] = {} def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # Give every subclass its own factors dict so mutating one class's # declaration can never leak into other models' resolved factors. if "factors" not in cls.__dict__: cls.factors = dict(cls.factors) def __init__(self, **factor_overrides: Any) -> None: self.factor_values = resolve_factors(self.factors, factor_overrides)
[docs] @abstractmethod def solve( self, scenario: Scenario, budget: Budget, rng: RngBundle, trace: Trace, ) -> ResultBundle: """Run the model, emitting checkpoints to ``trace``. Implementations must respect ``budget`` and record at least one checkpoint. Self-reported metrics go into checkpoint ``self_report`` entries; they are provenance, never scores. """
MODEL_REGISTRY: dict[str, type[TrafficAssignmentModel]] = {}
[docs] def register_model(cls: type[TrafficAssignmentModel]) -> type[TrafficAssignmentModel]: """Class decorator adding a model to the name registry (BO4Mob pattern). Only self-contained models belong here: the registry is what the CLI instantiates with no arguments, so a registered class must declare its ``name`` and ``capabilities`` at class level. Adapter-style models with per-instance capabilities (e.g. ``CallableModel``) are used by passing instances directly to ``run_experiment`` and must not be registered. """ if "name" not in cls.__dict__ or cls.name == "unnamed": raise TypeError(f"{cls.__qualname__} must declare a class-level `name`") if "capabilities" not in cls.__dict__: raise TypeError( f"{cls.__qualname__} must declare class-level `capabilities`; " "adapter-style models with per-instance capabilities should be " "passed to run_experiment directly instead of being registered." ) key = cls.name if key in MODEL_REGISTRY: raise ValueError(f"Model name {key!r} already registered") MODEL_REGISTRY[key] = cls return cls