Source code for phenotypic.tune._search_space._domains
"""Search-space domain types — a frozen pydantic discriminated union.
A tunable parameter's domain is one of ``Categorical`` / ``IntRange`` /
``FloatRange`` / ``Fixed``; each carries a ``kind`` literal so a ``Knob``'s
``domain`` field serializes and deserializes to the concrete type via the
``Domain`` discriminated union.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Annotated, Any, Literal, Union
from pydantic import BaseModel, ConfigDict, Field, model_validator
#: Closed set of domain discriminator tags (reused by the ``Domain`` union).
DomainKind = Literal["categorical", "int_range", "float_range", "fixed"]
class _DomainBase(BaseModel):
"""Shared config for every domain value-model (frozen, no extra fields)."""
model_config = ConfigDict(frozen=True, extra="forbid")
[docs]
class Categorical(_DomainBase):
"""A finite set of choices (bools, enum/literal members, strings, ...).
Args:
choices: The non-empty tuple of allowed values. Lists are coerced to
tuples so the model stays hashable and frozen.
"""
kind: Literal["categorical"] = "categorical"
choices: tuple[Any, ...]
@model_validator(mode="after")
def _non_empty(self) -> "Categorical":
if len(self.choices) == 0:
raise ValueError("Categorical requires at least one choice")
return self
[docs]
class IntRange(_DomainBase):
"""An integer range ``[low, high]`` with an optional step / log scale.
Args:
low: Inclusive lower bound.
high: Inclusive upper bound; must be ``>= low``.
step: Stride between sampled integers (default ``1``).
log: Whether to sample on a logarithmic scale (default ``False``).
"""
kind: Literal["int_range"] = "int_range"
low: int
high: int
step: int = 1
log: bool = False
@model_validator(mode="after")
def _ordered(self) -> "IntRange":
if self.high < self.low:
raise ValueError(f"IntRange high ({self.high}) < low ({self.low})")
return self
[docs]
def values(self) -> list[int]:
"""The discrete integers in ``[low, high]`` stepped by :attr:`step`.
The enumerable grid an ``IntRange`` exposes to the grid and random
strategies (``range(low, high + 1, step)`` — ``high`` inclusive). The
log scale is a *sampling* hint and does not change the enumerated set.
Returns:
The stepped integers from ``low`` to ``high`` inclusive.
"""
return list(range(self.low, self.high + 1, self.step))
[docs]
class FloatRange(_DomainBase):
"""A float range ``[low, high]`` with optional grid step / log scale.
Args:
low: Inclusive lower bound.
high: Inclusive upper bound; must be ``>= low``.
step: Optional strict stride for grid enumeration. ``None`` means the
range is continuous and not grid-enumerable. When the stride does
not land on ``high``, ``high`` is appended exactly.
log: Whether to sample on a logarithmic scale (default ``False``).
"""
kind: Literal["float_range"] = "float_range"
low: float
high: float
step: float | None = None
log: bool = False
@model_validator(mode="after")
def _ordered(self) -> "FloatRange":
if self.high < self.low:
raise ValueError(f"FloatRange high ({self.high}) < low ({self.low})")
if self.step is not None and self.step <= 0:
raise ValueError("FloatRange step must be positive when set")
return self
[docs]
def values(self) -> list[float]:
"""The deterministic strict-stride grid values for a stepped range.
Values start at ``low`` and advance by repeated ``step`` addition.
``high`` is appended exactly when the stride does not land on it.
Returns:
The inclusive float values from ``low`` to ``high``.
Raises:
ValueError: When this range is continuous (``step is None``).
"""
if self.step is None:
raise ValueError("continuous FloatRange has no enumerable values")
if self.low == self.high:
return [self.low]
low = Decimal(str(self.low))
high = Decimal(str(self.high))
step = Decimal(str(self.step))
values = [self.low]
value = low + step
while value < high:
values.append(float(value))
value += step
if values[-1] != self.high:
values.append(self.high)
return values
[docs]
class Fixed(_DomainBase):
"""A pinned (non-tunable / frozen) value.
Args:
value: The single value this knob is pinned to.
"""
kind: Literal["fixed"] = "fixed"
value: Any
#: The discriminated union a ``Knob``'s ``domain`` field uses.
Domain = Annotated[
Union[Categorical, IntRange, FloatRange, Fixed],
Field(discriminator="kind"),
]