"""Defines and samples the render-spec search space.""" from __future__ import annotations import random from dataclasses import replace from veil_pgd.config import OptimizerConfig from veil_pgd.types import Position, RenderSpec _POSITIONS: list[Position] = [ "bottom_middle", "top_middle", "top_right", "bottom_right", "top_left", "bottom_left", "center", ] _COLOR_STRATEGIES = ["bg_mean_offset", "near_bg_dark", "near_bg_light"] def grid_specs( decoys: list[str], positions: list[Position], cfg: OptimizerConfig ) -> list[RenderSpec]: """Coarse grid over structural knobs at a fixed mid alpha. Caps positions/colors to `cfg.grid_positions`/`cfg.grid_colors` so grid size stays bounded; the evolutionary phase explores the rest of the space. """ positions = positions[: cfg.grid_positions] colors = _COLOR_STRATEGIES[: cfg.grid_colors] specs: list[RenderSpec] = [] for text in decoys: for pos in positions: for color in colors: for px in cfg.grid_font_px: specs.append( RenderSpec( text=text, font_px=px, alpha=cfg.grid_alpha, color_strategy=color, # type: ignore[arg-type] position=pos, ) ) return specs def mutate(spec: RenderSpec, cfg: OptimizerConfig, rng: random.Random) -> RenderSpec: """Perturb the continuous/fine knobs for the evolutionary phase.""" lo, hi = cfg.alpha_range flo, fhi = cfg.font_px_range choice = rng.choice(["alpha", "font", "offset", "rep", "color"]) if choice == "alpha": return replace(spec, alpha=min(hi, max(lo, spec.alpha + rng.uniform(-0.05, 0.05)))) if choice == "font": return replace(spec, font_px=min(fhi, max(flo, spec.font_px + rng.choice([-2, -1, 1, 2])))) if choice == "offset": return replace(spec, brightness_offset=min(30, max(5, spec.brightness_offset + rng.choice([-5, 5])))) if choice == "rep": return replace(spec, repetition=min(4, max(1, spec.repetition + rng.choice([-1, 1])))) return replace(spec, color_strategy=rng.choice(_COLOR_STRATEGIES)) # type: ignore[arg-type] def default_positions() -> list[Position]: return list(_POSITIONS)