Source code for phenotypic.tune._search_space._space

"""The optimizer-facing search space: knobs + their domains."""
from __future__ import annotations

from typing import Any, Iterator, Literal, Mapping, Optional

from pydantic import BaseModel, ConfigDict, model_validator

from ._domains import Domain
from ._targets import KnobTarget, parse_key

#: Provenance of a knob's domain — a closed set (never a bare ``str``).
#: ``"manual"`` is the hand-authored default; the remaining tags are assigned by
#: ``infer_search_space`` (Phase 3) to record how each domain was derived. Phase 3
#: owns widening this alias if inference introduces further origins.
KnobSource = Literal[
    "manual",
    "tune_spec",
    "bool",
    "enum",
    "literal",
    "bounded",
    "unbounded_heuristic",
    "presence_optin",
]


[docs] class Knob(BaseModel): """One tunable parameter: a target, a domain, and optional provenance. Args: target: The structured ``KnobTarget`` identifying the parameter — a ``Param`` / ``Presence`` / ``Nested`` whose ``.key`` renders the canonical position-index path (e.g. ``"1.detectors[0].ignore_zeros"``, where the ``N.`` prefix is the operation's pipeline position). A legacy string ``key="0.sigma"`` is still accepted and coerced to a target (string-preserving round-trip), so older call sites and frozen ``tuning_spec.json`` blobs keep working without migration. domain: The value space to search over (the ``Domain`` discriminated union — ``Categorical`` / ``IntRange`` / ``FloatRange`` / ``Fixed``). conditional_on: Parent presence conditions that gate this knob; the knob is active only when each ``(target, value)`` pair holds, e.g. ``((Presence(op=0, op_class="GaussianBlur"), True),)`` — define-by-run conditional nesting. A string parent (``"0.GaussianBlur.__enabled__"``) is coerced to a target. ``None`` means unconditional. source: Provenance of the knob (a closed ``KnobSource`` set). Defaults to ``"manual"`` for hand-authored spaces; ``infer_search_space`` (Phase 3) populates it with the inference origin. needs_review: Whether a human should confirm this knob before tuning (set by inference for shaky guesses); defaults to ``False``. description: Human-readable description, auto-sourced from the owning class's ``model_json_schema()`` during inference; ``""`` by default. """ model_config = ConfigDict(frozen=True, extra="forbid") target: KnobTarget domain: Domain conditional_on: Optional[tuple[tuple[KnobTarget, Any], ...]] = None source: KnobSource = "manual" needs_review: bool = False description: str = "" @model_validator(mode="before") @classmethod def _coerce_legacy_strings(cls, data: Any) -> Any: """Accept a legacy string ``key=`` and string ``conditional_on`` parents. ``key="0.sigma"`` is coerced to ``target=parse_key("0.sigma")`` and removed from the input (so ``extra="forbid"`` does not trip). A ``conditional_on`` entry whose parent is a string is parsed the same way; a dict (structured JSON) or a target instance passes through to the discriminated-union validator unchanged. """ if not isinstance(data, dict): return data data = dict(data) if "key" in data and "target" not in data: data["target"] = parse_key(data.pop("key")) cond = data.get("conditional_on") if cond is not None: data["conditional_on"] = tuple( (parse_key(parent) if isinstance(parent, str) else parent, value) for parent, value in cond ) return data @property def key(self) -> str: """The canonical key string of this knob's target (back-compat read).""" return self.target.key
[docs] def is_active(self, chosen: Mapping[str, Any]) -> bool: """Whether this knob's parent presence conditions hold in ``chosen``. An unconditional knob (``conditional_on is None``) is always active; a conditional knob is active only when **every** ``(parent_target, value)`` pair in :attr:`conditional_on` matches what was already chosen this trial (define-by-run conditional nesting). The single predicate every strategy (grid / random / Optuna) gates conditional knobs by. Args: chosen: The parameter values already assigned this trial (``{key: value}``); a missing parent key never matches. Returns: ``True`` if the knob should be assigned given ``chosen``. """ if self.conditional_on is None: return True return all(chosen.get(ptarget.key) == pval for ptarget, pval in self.conditional_on)
[docs] class SearchSpace(BaseModel): """The clean, optimizer-facing collection of tunable knobs. Args: knobs: The ordered tuple of ``Knob`` instances the optimizer searches. Note: ``__iter__`` is overridden to yield ``Knob`` instances (not pydantic's default ``(field_name, value)`` pairs), so ``dict(space)`` does **not** produce a model dict — use ``model_dump()`` for serialization. """ model_config = ConfigDict(frozen=True, extra="forbid") knobs: tuple[Knob, ...]
[docs] def keys(self) -> list[str]: """Return the knob keys in declaration order.""" return [k.key for k in self.knobs]
[docs] def targets(self) -> list[KnobTarget]: """Return the knob targets in declaration order.""" return [k.target for k in self.knobs]
[docs] def domain(self, key: str) -> Domain: """Return the domain for ``key``; raise ``KeyError`` if absent.""" for k in self.knobs: if k.key == key: return k.domain raise KeyError(key)
def __iter__(self) -> Iterator[Knob]: # type: ignore[override] return iter(self.knobs)