"""Landscape template library (v1 template-picker LandscapeForge). Each template has a hand-written analytic gradient — no autodiff required. All templates are guaranteed differentiable, finite, and bounded below on the typical init region x ~ N(0, 0.5^2 I). """ from dataclasses import dataclass, field from typing import Callable, Literal import numpy as np TemplateName = Literal[ "quadratic", "styblinski_tang", "huber", "gaussian_mix", "himmelblau", "rosenbrock", "stiff_quadratic", "plateau", "cliff", ] Tier = Literal["T0", "T1", "T2"] TIER_MENU: dict[str, list[str]] = { "T0": ["quadratic", "styblinski_tang", "huber"], "T1": ["quadratic", "styblinski_tang", "huber", "gaussian_mix", "himmelblau"], "T2": ["quadratic", "styblinski_tang", "huber", "gaussian_mix", "himmelblau", "rosenbrock", "stiff_quadratic", "plateau", "cliff"], } @dataclass class Landscape: name: str dim: int params: dict f: Callable[[np.ndarray], float] grad: Callable[[np.ndarray], np.ndarray] f_min: float = 0.0 # known global minimum value description: str = "" # ---------- template constructors ---------- def make_quadratic(dim: int, cond: float = 1.0, rng: np.random.Generator | None = None) -> Landscape: """f(x) = 0.5 * x^T A x with diag Hessian of condition number `cond`.""" diag = np.linspace(1.0, float(cond), dim) A = np.diag(diag) def f(x): return float(0.5 * x @ A @ x) def grad(x): return A @ x return Landscape( name="quadratic", dim=dim, params={"cond": cond}, f=f, grad=grad, f_min=0.0, description=f"Convex quadratic in R^{dim}, condition number {cond:.1f}.", ) def make_stiff_quadratic(dim: int, cond: float = 1000.0, **_) -> Landscape: return make_quadratic(dim, cond) # alias with higher cond def make_styblinski_tang(dim: int, **_) -> Landscape: """f(x) = 0.5 * sum(x^4 - 16 x^2 + 5 x), min at x_i ≈ -2.903534.""" def f(x): return float(0.5 * np.sum(x**4 - 16.0 * x**2 + 5.0 * x)) def grad(x): return 0.5 * (4.0 * x**3 - 32.0 * x + 5.0) f_min = dim * 0.5 * ((-2.903534)**4 - 16.0 * (-2.903534)**2 + 5.0 * (-2.903534)) return Landscape( name="styblinski_tang", dim=dim, params={}, f=f, grad=grad, f_min=float(f_min), description=f"Styblinski-Tang in R^{dim}, multimodal with global min at x_i ≈ -2.9.", ) def make_huber(dim: int, delta: float = 1.0, **_) -> Landscape: """Smooth Huber-ish loss: f(x) = sum(delta^2 * (sqrt(1 + (x/delta)^2) - 1)). Smooth everywhere (unlike piecewise Huber). Behaves like 0.5 x^2 near 0, linear for |x| >> delta. """ def f(x): return float(np.sum(delta**2 * (np.sqrt(1.0 + (x/delta)**2) - 1.0))) def grad(x): return x / np.sqrt(1.0 + (x/delta)**2) return Landscape( name="huber", dim=dim, params={"delta": delta}, f=f, grad=grad, f_min=0.0, description=f"Smooth pseudo-Huber in R^{dim}, delta={delta}.", ) def make_rosenbrock(dim: int, **_) -> Landscape: """Classic stiff-valley Rosenbrock.""" assert dim >= 2 def f(x): return float(np.sum(100.0 * (x[1:] - x[:-1]**2)**2 + (1.0 - x[:-1])**2)) def grad(x): g = np.zeros_like(x) g[:-1] += -400.0 * x[:-1] * (x[1:] - x[:-1]**2) - 2.0 * (1.0 - x[:-1]) g[1:] += 200.0 * (x[1:] - x[:-1]**2) return g return Landscape( name="rosenbrock", dim=dim, params={}, f=f, grad=grad, f_min=0.0, description=f"Rosenbrock (curved stiff valley) in R^{dim}, min at (1,..,1).", ) def make_gaussian_mix(dim: int, k: int = 3, sigma: float = 0.5, spread: float = 2.0, rng: np.random.Generator | None = None, **_) -> Landscape: """f(x) = -sum_j w_j * exp(-||x - c_j||^2 / (2 sigma^2)). Negated so minima are where mixture density is highest. Bounded below by 0. """ rng = rng if rng is not None else np.random.default_rng(0) centers = rng.normal(0.0, spread, size=(k, dim)) weights = np.ones(k) / k # uniform; one of these is the "global" min s2 = sigma * sigma def f(x): d2 = np.sum((centers - x)**2, axis=1) # (k,) return float(-np.sum(weights * np.exp(-d2 / (2.0 * s2)))) def grad(x): diffs = x - centers # (k, dim) d2 = np.sum(diffs**2, axis=1) # (k,) e = np.exp(-d2 / (2.0 * s2)) # (k,) # d/dx [-w_j exp(-||x-c_j||^2 / 2s^2)] = w_j * (x-c_j) / s^2 * exp(...) return (weights * e / s2)[:, None] * diffs # broadcast, sum over k below # Wait — need to sum over components: # Fix grad to properly sum: def grad_correct(x): diffs = x - centers d2 = np.sum(diffs**2, axis=1) e = np.exp(-d2 / (2.0 * s2)) coeff = weights * e / s2 return np.sum(coeff[:, None] * diffs, axis=0) # Global min approx: evaluate at each center, return the lowest f. f_min = float(min(f(c) for c in centers)) return Landscape( name="gaussian_mix", dim=dim, params={"k": k, "sigma": sigma, "spread": spread, "centers": centers.tolist()}, f=f, grad=grad_correct, f_min=f_min, description=f"Negative Gaussian mixture in R^{dim}, k={k} modes, sigma={sigma}, spread={spread}.", ) def make_himmelblau(dim: int = 2, **_) -> Landscape: """f(x,y) = (x^2+y-11)^2 + (x+y^2-7)^2. 4 global minima at value 0.""" assert dim == 2, "Himmelblau is 2D only" def f(x): return float((x[0]**2 + x[1] - 11.0)**2 + (x[0] + x[1]**2 - 7.0)**2) def grad(x): gx = 4.0 * x[0] * (x[0]**2 + x[1] - 11.0) + 2.0 * (x[0] + x[1]**2 - 7.0) gy = 2.0 * (x[0]**2 + x[1] - 11.0) + 4.0 * x[1] * (x[0] + x[1]**2 - 7.0) return np.array([gx, gy]) return Landscape( name="himmelblau", dim=2, params={}, f=f, grad=grad, f_min=0.0, description="Himmelblau (2D), four global minima at value 0.", ) def make_plateau(dim: int, radius: float = 1.0, **_) -> Landscape: """Smooth plateau: f = tanh((||x||^2 - r^2) / r^2). Near-zero gradient far from boundary.""" r2 = radius * radius def f(x): return float(np.tanh((np.sum(x**2) - r2) / r2)) def grad(x): u = (np.sum(x**2) - r2) / r2 return (1.0 - np.tanh(u)**2) * (2.0 * x / r2) return Landscape( name="plateau", dim=dim, params={"radius": radius}, f=f, grad=grad, f_min=-1.0, description=f"Plateau landscape in R^{dim}, radius {radius}, vanishing gradient at center.", ) def make_cliff(dim: int, **_) -> Landscape: """Smooth cliff: quadratic + tanh step. Tough for fixed-step optimizers.""" def f(x): s = np.sum(x) return float(0.5 * np.sum(x**2) + 5.0 * np.tanh(s)) def grad(x): s = np.sum(x) t = 1.0 - np.tanh(s)**2 return x + 5.0 * t * np.ones_like(x) return Landscape( name="cliff", dim=dim, params={}, f=f, grad=grad, f_min=-5.0, # approximate lower bound description=f"Quadratic with tanh cliff in R^{dim}.", ) BUILDERS: dict[str, Callable[..., Landscape]] = { "quadratic": make_quadratic, "stiff_quadratic": make_stiff_quadratic, "styblinski_tang": make_styblinski_tang, "huber": make_huber, "rosenbrock": make_rosenbrock, "gaussian_mix": make_gaussian_mix, "himmelblau": make_himmelblau, "plateau": make_plateau, "cliff": make_cliff, } def build_landscape(template: str, dim: int, params: dict | None = None, rng: np.random.Generator | None = None) -> Landscape: """Instantiate a landscape by name.""" if template not in BUILDERS: raise ValueError(f"Unknown template {template!r}; known: {list(BUILDERS)}") return BUILDERS[template](dim=dim, rng=rng, **(params or {})) def structural_hints(ls: Landscape, n_samples: int = 200, rng: np.random.Generator | None = None) -> dict: """Env-computed hints: Lipschitz estimate, gradient spread, modality hint. Sampled at reset, exposed to OptCoder as free info. """ rng = rng if rng is not None else np.random.default_rng(0) xs = rng.normal(0.0, 1.0, size=(n_samples, ls.dim)) fs = np.array([ls.f(x) for x in xs]) gs = np.array([ls.grad(x) for x in xs]) g_norms = np.linalg.norm(gs, axis=1) return { "lipschitz_estimate": float(np.percentile(g_norms, 95)), "grad_norm_median": float(np.median(g_norms)), "f_range": [float(fs.min()), float(fs.max())], "f_median": float(np.median(fs)), # crude modality: count local f peaks on random 1D slices "modality_hint": _modality_hint(ls, rng), } def _modality_hint(ls: Landscape, rng: np.random.Generator) -> str: """Very crude multimodality probe on 5 random 1D slices.""" hits = 0 for _ in range(5): center = rng.normal(0.0, 0.5, size=ls.dim) direction = rng.normal(0.0, 1.0, size=ls.dim) direction /= np.linalg.norm(direction) + 1e-12 ts = np.linspace(-3.0, 3.0, 30) vals = np.array([ls.f(center + t * direction) for t in ts]) # count sign changes in finite diff d = np.diff(vals) s = np.sign(d) sign_changes = int(np.sum(s[1:] != s[:-1])) if sign_changes >= 3: hits += 1 if hits >= 3: return "multimodal" if hits >= 1: return "possibly_multimodal" return "unimodal"