Files
2026-06-26 20:50:07 +08:00

74 lines
2.6 KiB
Python

"""Declared search space — every tunable parameter in one place (doc 05 §2).
A search space is a mapping ``name -> (low, high, step)`` plus a set of
integer parameter names. The optimizer turns each entry into an Optuna
``suggest_float`` / ``suggest_int``. Nothing tunable should be hidden in the
strategy body.
Rules (doc 05 §2):
- Ranges encode priors, not hope — bracket where the answer plausibly is.
- Step matters — too fine explodes the space, too coarse misses optima.
- What is NOT in the space is a decision — list exclusions explicitly.
- Timeframe is usually fixed per iteration (searching timeframes overfits).
"""
from __future__ import annotations
from typing import TypedDict
class Range(TypedDict):
"""One parameter range: ``low`` to ``high`` in steps of ``step``."""
low: float
high: float
step: float
# name -> (low, high, step)
SearchSpace = dict[str, tuple[float, float, float]]
def suggest_params(
trial,
space: SearchSpace,
int_params: set[str] | None = None,
) -> dict[str, float | int]:
"""Sample every parameter in ``space`` from an Optuna trial.
Integer params (listed in ``int_params``) use ``suggest_int``; floats use
``suggest_float`` with ``step``. Returns a plain ``dict`` of sampled
values keyed by parameter name.
"""
int_params = int_params or set()
sampled: dict[str, float | int] = {}
for name, (low, high, step) in space.items():
if name in int_params:
lo = int(round(low))
hi = int(round(high))
st = max(int(round(step)), 1)
sampled[name] = trial.suggest_int(name, lo, hi, step=st)
else:
sampled[name] = trial.suggest_float(name, low, high, step=step)
return sampled
def validate_space(space: SearchSpace, int_params: set[str] | None = None) -> list[str]:
"""Return a list of problems with the space (empty = OK).
Catches: reversed ranges, zero/negative steps, int params with non-integer
bounds, duplicate names. Run this once before launching a study so a
malformed space doesn't waste a background run.
"""
int_params = int_params or set()
problems: list[str] = []
for name, (low, high, step) in space.items():
if high < low:
problems.append(f"{name}: high < low ({low} > {high})")
if step <= 0:
problems.append(f"{name}: step <= 0 ({step})")
if name in int_params:
if int(low) != low or int(high) != high:
problems.append(f"{name}: int param with non-integer bound ({low}, {high})")
return problems