Module Algostream_optimization.Search_space

Parameter search spaces.

A point is a (string * float) list — the flat representation Strategy.S.params_of_assoc consumes. Keeping it flat is what lets the optimizer traverse any strategy's parameters without knowing the concrete params type, and without existentials or GADTs. An integer dimension is a float that the strategy's own params_of_assoc rejects if non-integral.

module Rng = Algostream_rng.Rng
type spec =
  1. | Grid of float array
    (*

    an explicit set of values

    *)
  2. | Uniform of {
    1. lo : float;
    2. hi : float;
    }
  3. | Log_uniform of {
    1. lo : float;
    2. hi : float;
    }
    (*

    for scale parameters — window lengths, notionals — where the interesting variation is multiplicative. lo must be positive.

    *)
  4. | Int_range of {
    1. lo : int;
    2. hi : int;
    3. step : int;
    }
type dim = {
  1. name : string;
  2. spec : spec;
}
type t = dim list
val of_bounds : (string * float * float) list -> ?points_per_dim:int -> unit -> t

Build a default space from a strategy's declared param_bounds, discretizing each continuous dimension into points_per_dim grid values.

val cardinality : t -> int option

Number of grid points, or None if any dimension is continuous.

val grid_points : t -> max_points:int -> ((string * float) list array, [ `Too_large of int ]) Stdlib.result

Full Cartesian product. Returns `Too_large n rather than allocating when the product exceeds max_points — a silent truncation would report a "best" that never searched most of the space.

val sample : t -> Rng.t -> (string * float) list

One uniformly random point.

val random_points : t -> Rng.t -> n:int -> (string * float) list array

n random points.

val stratified_points : t -> Rng.t -> n:int -> (string * float) list array

n Latin-hypercube points: each dimension is cut into n equal strata and every stratum receives exactly one sample.

What this buys, precisely: exact marginal coverage — no axis is left with a large unsampled gap, which independent draws routinely produce. What it does not buy is guaranteed joint coverage, so on a space whose optimum sits at one specific combination it is not reliably better than random search at finding that cell. The advantage shows on smooth objectives and grows with dimension, where random draws leave increasingly ragged marginals.

Chosen over a Sobol sequence, which would give better joint coverage, because Sobol needs per-dimension direction-number tables: a single mis-transcribed row degrades the sequence silently while it still looks valid. This is correct by construction and checkable — see test_stratified_covers_every_stratum. Applies at any dimension; there is no table to run out of.

val stratified_supported : t -> bool

Always true; retained so callers can branch uniformly.

val neighbours : t -> (string * float) list -> (string * float) list array

Immediate grid neighbours of a point, for coordinate descent.

val clamp : t -> (string * float) list -> (string * float) list

Clamp a point into the space, for optimizers that can step outside it.

val to_string : t -> string