Spaces:
Sleeping
Sleeping
File size: 7,082 Bytes
5146e76 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """Held-out partner catalog.
Reads ``evaluation/configs/global_heldout_settings.yaml`` as the source
of truth so the benchmark_ui leaderboard and the training pipeline
agree on which agents count as "held-out". Versioning is per (env,
version_tag) tuples.
For envs whose adapters are already registered (Hanabi, LBF, Mock),
this module maps yaml partner entries to callables by delegating to
``EnvRenderer.available_partners()``. That keeps one source of truth
for partner construction (the env adapter) and lets the yaml add
bounds / inclusion-per-version metadata on top.
Version discovery:
Today the yaml is unversioned. We treat each env's block as
version "v1" by convention. When Caroline adds an explicit ``version:``
field we'll honor it here.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
from .envs import get as get_env, registry as env_registry
log = logging.getLogger(__name__)
_REPO_ROOT = Path(__file__).resolve().parents[2]
_HELDOUT_YAML = _REPO_ROOT / "evaluation" / "configs" / "global_heldout_settings.yaml"
@dataclass
class HeldoutPartner:
"""One held-out partner as far as the leaderboard cares.
``load_get_action`` returns the ``(obs, state, rng) -> action``
callable the eval loop needs. We delegate to the env adapter's
partner spec when possible so we aren't duplicating construction
logic (SmartBot card_counts, OBL weight paths, etc.).
"""
env_name: str
version: str
key: str
display_name: str
difficulty: str
description: str
normalize_bounds: tuple[float, float] | None
tags: list[str] = field(default_factory=list)
_load_get_action: Callable[[], Callable] | None = field(default=None, repr=False)
def load_get_action(self):
if self._load_get_action is None:
raise RuntimeError(
f"held-out partner {self.env_name}:{self.key} has no "
"construction hook; env adapter must register it."
)
adapter = self._load_get_action()
# Adapter returns a "partner wrapper" object with .get_action().
# We want a plain (obs, state, rng) -> int callable.
if hasattr(adapter, "get_action"):
return adapter.get_action
if callable(adapter):
return adapter
raise TypeError(f"partner {self.key} adapter has no get_action")
# Cache: parsed yaml + computed HeldoutPartner lists.
_CACHE: dict[tuple[str, str], list[HeldoutPartner]] = {}
def _parse_yaml() -> dict:
"""Parse the yaml once on first use. We don't import pyyaml at
module load time in case it's absent on a minimal install."""
try:
import yaml # noqa: WPS433
except ImportError as exc: # pragma: no cover
raise ImportError(
"pyyaml required to read global_heldout_settings.yaml; "
"pip install pyyaml"
) from exc
if not _HELDOUT_YAML.exists():
log.warning("held-out yaml not found at %s", _HELDOUT_YAML)
return {}
return yaml.safe_load(_HELDOUT_YAML.read_text()) or {}
def _env_block(env_name: str) -> dict:
data = _parse_yaml()
heldout_set = data.get("heldout_set") or {}
return heldout_set.get(env_name) or {}
def _normalize_bounds(entry: dict) -> tuple[float, float] | None:
"""Best-effort: pick the first (lo, hi) pair we can find under
performance_bounds, preferring returned_episode_returns."""
pb = entry.get("performance_bounds") or {}
for key in ("returned_episode_returns", "base_return", "percent_eaten"):
if key not in pb:
continue
val = pb[key]
# Could be [lo, hi] or list-of-pairs for ego-multi-seed envs.
if isinstance(val, list) and val:
first = val[0]
if isinstance(first, list) and len(first) == 2:
return float(first[0]), float(first[1])
if len(val) == 2 and not isinstance(first, list):
return float(val[0]), float(val[1])
return None
def load_heldout_partners(env_name: str, version: str = "v1") -> list[HeldoutPartner]:
"""Build the held-out catalog for (env, version). Cached on first
call per (env, version)."""
cache_key = (env_name, version)
if cache_key in _CACHE:
return _CACHE[cache_key]
registry = env_registry()
adapter = registry.get(env_name)
if adapter is None:
raise KeyError(f"env '{env_name}' has no registered adapter")
# Build a lookup of adapter-provided PartnerSpecs by key.
spec_by_key = {p.key: p for p in adapter.available_partners()}
# Read yaml for bounds + any version-gate metadata.
yaml_partners = _env_block(env_name)
partners: list[HeldoutPartner] = []
if yaml_partners:
# For each yaml entry whose key matches an adapter partner,
# create a HeldoutPartner. Entries without adapter support are
# skipped with a log (e.g. "ippo_seed123" type RL-fixture entries
# that the UI play-mode catalog doesn't expose).
for yaml_key, yaml_entry in yaml_partners.items():
if not isinstance(yaml_entry, dict):
continue
# Pick a matching adapter partner. For keys we've aligned
# (iggi, piers, smartbot, etc.) the yaml key == adapter key.
# For others we skip.
spec = spec_by_key.get(yaml_key)
if spec is None:
log.debug("skipping %s:%s: no adapter partner", env_name, yaml_key)
continue
partners.append(HeldoutPartner(
env_name=env_name,
version=version,
key=spec.key,
display_name=spec.display_name,
difficulty=spec.difficulty,
description=spec.description,
normalize_bounds=_normalize_bounds(yaml_entry),
tags=list(spec.tags),
_load_get_action=spec.load_fn,
))
# If the yaml is empty for this env (e.g. mock, or a fresh env with
# no yaml block), fall back to every partner the adapter exposes.
if not partners:
for spec in adapter.available_partners():
partners.append(HeldoutPartner(
env_name=env_name,
version=version,
key=spec.key,
display_name=spec.display_name,
difficulty=spec.difficulty,
description=spec.description,
normalize_bounds=None,
tags=list(spec.tags),
_load_get_action=spec.load_fn,
))
_CACHE[cache_key] = partners
return partners
def list_versions(env_name: str) -> list[str]:
"""Placeholder until the yaml has per-env version tags. Every env
has one version ("v1") today."""
try:
get_env(env_name)
except KeyError:
return []
return ["v1"]
def invalidate_cache() -> None:
"""Test-only hook so fixtures can reload the yaml."""
_CACHE.clear()
|