Supersaturated and Space-Filling Designs ======================================== Two families sit at opposite ends of the run budget. A supersaturated design screens more factors than it has runs; a space-filling design spends its runs on covering the region evenly, with no model in mind. Supersaturated designs: more factors than runs ---------------------------------------------- With ``k`` factors, any design that estimates every main effect needs at least ``k + 1`` runs. A supersaturated design uses fewer, so the columns cannot all be orthogonal. It works under *effect sparsity*: when only a few of the many factors matter, a selection method (stepwise regression, the lasso) can still find them. ``generate_design`` chooses one automatically when the budget is below ``k + 1``: .. code-block:: python from process_improve.experiments import Factor, generate_design factors = [Factor(name=f"X{i + 1}", low=0, high=1) for i in range(22)] result = generate_design(factors, budget=12) m = result.metadata print(result.design_type, result.n_runs) # supersaturated 12 print(round(m["e_s2"], 2), round(m["e_s2_lower_bound"], 2)) # 6.86 6.86 print(m["max_abs_s"], m["n_fully_aliased_pairs"]) # 4 0 The quality measure is ``E(s^2)``, the average over all pairs of columns of ``s_ij^2``, where ``s_ij = x_i' x_j`` is zero for orthogonal columns. This 12-run design for 22 factors reaches the theoretical lower bound for balanced designs. The largest ``|s_ij|`` is 4 out of a possible 12, and no pair of columns is fully aliased. The reported ``e_s2_efficiency`` is the bound divided by ``E(s^2)``. The bound is not attainable at every size, so a value below 1 does not by itself mean that a better balanced design exists. Fifteen factors in the same 12 runs show the gap: .. code-block:: python fifteen = [Factor(name=f"X{i + 1}", low=0, high=1) for i in range(15)] m = generate_design(fifteen, "supersaturated", budget=12).metadata print(round(m["e_s2"], 2), round(m["e_s2_lower_bound"], 2)) # 5.94 3.74 print(round(m["e_s2_efficiency"], 2)) # 0.63 The construction is Lin's (1993): take a Hadamard matrix of order ``2n``, keep the ``n`` rows where one *branching* column is +1, and drop that column. Every branching column is tried and the best half-fraction is kept. Hadamard matrices built by Kronecker products (orders 16, 24, 32 in pyDOE3) leave identical columns in their half-fractions, so Paley's construction is used wherever it exists (orders ``N`` with ``N - 1`` a prime congruent to 3 mod 4, including 12, 20, 24, 32, 44 and 60). When a requested run count forces fully aliased pairs, a warning names the run count that avoids them. Space-filling designs: no model assumed --------------------------------------- Space-filling designs suit computer experiments, Gaussian-process and machine-learning surrogates, and exploration where the shape of the response is unknown. ``budget`` sets the number of runs; the default is ``10 * k``. .. code-block:: python box = [Factor(name=n, low=0, high=10) for n in "ABC"] for design_type in ["latin_hypercube", "maximin_lhs", "uniform", "sobol", "halton", "maximin"]: m = generate_design(box, design_type=design_type, budget=20, random_state=0).metadata print(design_type, round(m["min_distance"], 2), round(m["centered_l2_discrepancy"], 4)) .. list-table:: Twenty runs in three factors (coded distances) :header-rows: 1 * - Design type - Smallest distance between runs (higher is more spread) - Squared centred L2 discrepancy (lower is more uniform) * - ``latin_hypercube`` - 0.21 - 0.0062 * - ``maximin_lhs`` - 0.68 - 0.0030 * - ``uniform`` - 0.47 - 0.0023 * - ``sobol`` - 0.35 - 0.0051 * - ``halton`` - 0.40 - 0.0039 * - ``maximin`` - 1.00 - 0.1025 The two columns measure different things. A maximin design keeps runs as far apart as possible, which pushes them to the faces and corners of the region; in 8 or more factors every run of a ``"maximin"`` design sits on the levels -1, 0 and +1, so runs coincide when projected onto a few factors. A uniform design matches the fraction of runs in every sub-box to that sub-box's volume. The discrepancy column is Hickernell's squared centred L2 discrepancy, ``CD^2``, the value ``scipy.stats.qmc.discrepancy`` returns and ``metadata["centered_l2_discrepancy"]`` reports. The Latin hypercube types also guarantee that each factor's range is cut into ``n`` slices with one run in each, which the maximin and sequence designs do not. Constrained regions and mixtures -------------------------------- ``"sobol"``, ``"halton"`` and ``"maximin"`` also work inside a constrained region or a constrained mixture, through :class:`~process_improve.experiments.DesignRegion`: .. code-block:: python from process_improve.experiments import Constraint factors = [Factor(name="T", low=100, high=150), Factor(name="D", low=20, high=60)] heat = Constraint(expression="3*T + 5*D <= 600") result = generate_design(factors, design_type="maximin", budget=12, constraints=[heat]) * ``"sobol"`` and ``"halton"`` map the sequence onto the region (onto the simplex for a mixture, through a map that keeps uniform points uniform) and skip its infeasible points. * ``"maximin"`` chooses the runs from a dense uniform sample of the region plus its boundary points, by a farthest-point build followed by exchanges. The Latin hypercube types refuse a constrained or mixture region: their guarantee is about each factor's range, which a constraint cuts into pieces of unequal length.