Skip to content

Design of experiments

Space-filling, classical and optimal designs.

doe

Design of Experiments (DoE) - Initial sampling strategies for Bayesian optimization.

This module provides methods for generating initial experimental designs before starting the optimization loop. Supported methods:

Space-filling: - Random sampling - Latin Hypercube Sampling (LHS) - Sobol sequences - Halton sequences - Hammersly sequences

Classical RSM: - Full Factorial - Fractional Factorial - Central Composite Design (CCD) - Box-Behnken

Screening: - Plackett-Burman (ultra-efficient 2-level main-effect screening) - Generalized Subset Design (fractional factorial for mixed/multi-level factors)

DesignNotEstimableError

Bases: ValueError

A constrained classical design lost points its implied model needs.

generate_initial_design

generate_initial_design(search_space: SearchSpace, method: Literal['random', 'lhs', 'sobol', 'halton', 'hammersly', 'full_factorial', 'fractional_factorial', 'ccd', 'box_behnken', 'plackett_burman', 'gsd', 'optimal'] = 'lhs', n_points: Optional[int] = None, random_seed: Optional[int] = None, lhs_criterion: str = 'maximin', n_levels: int = 2, n_center: int = 1, generators: Optional[str] = None, ccd_alpha: str = 'orthogonal', ccd_face: str = 'circumscribed', gsd_reduction: int = 2, model_type: Optional[str] = None, effects: Optional[List[str]] = None, criterion: str = 'D', algorithm: str = 'fedorov', max_iter: int = 200, allow_infeasible: bool = False) -> List[Dict[str, Any]]

Generate initial experimental design using specified sampling strategy.

This function creates a set of experimental conditions to evaluate before starting Bayesian optimization.

Space-filling methods (take n_points as input): - random: Uniform random sampling - lhs: Latin Hypercube Sampling (recommended for most cases) - sobol: Sobol quasi-random sequences (low discrepancy) - halton: Halton sequences (via Hammersly sampler) - hammersly: Hammersly sequences (low discrepancy)

Classical RSM methods (run count determined by design structure): - full_factorial: All combinations of factor levels - fractional_factorial: Subset of full factorial using generators - ccd: Central Composite Design (factorial + axial + center) - box_behnken: Box-Behnken design (3+ continuous factors)

Screening methods (run count determined by design structure): - plackett_burman: Ultra-efficient 2-level screening (continuous only) - gsd: Generalized Subset Design (supports mixed categorical/continuous)

Optimal design (user-specified model structure): - optimal: Generate a statistically efficient design optimized for estimating specific model terms (main effects, interactions, quadratic terms). Requires n_points and either model_type or effects.

Parameters:

Name Type Description Default
search_space SearchSpace

SearchSpace object with defined variables

required
method Literal['random', 'lhs', 'sobol', 'halton', 'hammersly', 'full_factorial', 'fractional_factorial', 'ccd', 'box_behnken', 'plackett_burman', 'gsd', 'optimal']

Sampling method to use

'lhs'
n_points Optional[int]

Number of points (default 10 for space-filling; required for optimal; ignored for classical)

None
random_seed Optional[int]

Random seed for reproducibility

None
lhs_criterion str

Criterion for LHS ("maximin", "correlation", "ratio")

'maximin'
n_levels int

Levels per continuous factor for full factorial (2 or 3), or candidate grid resolution for optimal design (default 5 for optimal, 2 for factorial)

2
n_center int

Number of center point replicates (classical designs)

1
generators Optional[str]

Fractional factorial generator string (e.g. "a b ab")

None
ccd_alpha str

CCD alpha type ("orthogonal" or "rotatable")

'orthogonal'
ccd_face str

CCD face type ("circumscribed", "inscribed", or "faced")

'circumscribed'
gsd_reduction int

GSD reduction factor (>=2); larger means fewer runs

2
model_type Optional[str]

Optimal design model shortcut. One of "linear" (main effects only), "interaction" (main + all pairwise), or "quadratic" (main + pairwise + squared terms for continuous vars). Used only when method="optimal".

None
effects Optional[List[str]]

Optimal design custom effects list. Strings using variable names from the SearchSpace. Format rules:

  • Main effects: "Temperature", "Pressure"
  • Interactions: "Temperature*Pressure"
  • Quadratic terms: "Temperature**2"

Used only when method="optimal". Specify either model_type or effects, not both.

None
criterion str

Optimality criterion ("D", "A", or "I"). Used only when method="optimal". Default "D".

'D'
algorithm str

Optimal design algorithm. One of "sequential", "simple_exchange", "fedorov" (default), "modified_fedorov", "detmax". Used only when method="optimal".

'fedorov'
max_iter int

Maximum iterations for optimal design exchange algorithms. Default 200. Used only when method="optimal".

200
allow_infeasible bool

For constrained classical designs, return the feasible remnant with a warning even when the design's implied model is no longer estimable. Default False raises instead.

False

Returns:

Type Description
List[Dict[str, Any]]

List of dictionaries, each containing variable names and values.

List[Dict[str, Any]]

Does NOT include 'Output' column - experiments need to be evaluated.

Raises:

Type Description
ValueError

If search_space has no variables, method is unknown, or design is incompatible with the search space

get_design_info

get_design_info(method: str, search_space: SearchSpace, n_levels: int = 2, n_center: int = 1, generators: Optional[str] = None, ccd_alpha: str = 'orthogonal', ccd_face: str = 'circumscribed', gsd_reduction: int = 2, model_type: Optional[str] = None, effects: Optional[List[str]] = None, criterion: str = 'D', algorithm: str = 'fedorov', n_points: Optional[int] = None) -> Optional[Dict[str, Any]]

Return metadata about the design structure for a given method.

Returns None for space-filling methods.

optimal_design

Optimal Experimental Design (OED) for ALchemist.

This module provides optimal design algorithms that generate statistically efficient experimental designs based on user-specified model structures. Unlike space-filling or classical RSM designs, optimal designs allow users to specify which main effects, interactions, and quadratic terms they expect to be important, then generate the most information-rich set of experiments to estimate those specific model terms.

The module wraps pyDOE's doe_optimal subpackage, adding:

  • Custom model specification: Users define effects using variable names (e.g., "Temperature*Pressure") instead of abstract polynomial degrees.
  • Mixed variable support: Candidate set generation for continuous and categorical variables together.
  • ALchemist integration: Direct mapping from SearchSpace variables to design points as List[Dict].

Supported optimality criteria:

  • D-optimal: Maximize |X'X| — best for parameter estimation.
  • A-optimal: Minimize trace((X'X)^-1) — minimize average parameter variance.
  • I-optimal: Minimize integrated prediction variance over the design space.

Supported algorithms:

  • sequential (Dykstra) — Greedy, fastest.
  • simple_exchange (Wynn-Mitchell) — Drop-add exchange.
  • fedorov — Full pairwise exchange (recommended default).
  • modified_fedorov — Position-wise replacement.
  • detmax (Mitchell) — Exchange with excursions, best quality.

.. warning::

**Experimental Feature — Categorical Variables**: Optimal design with
categorical variables uses dummy coding (k-1 indicator columns per
variable, with the first category as the reference level) in the design
matrix. The statistical properties of optimal designs with categorical
variables are an active area of research and may not behave identically
to classical continuous-variable optimal designs. Results should be
reviewed carefully. For well-established categorical screening, consider
using Full Factorial or GSD methods instead.
References
  • Atkinson, A. C., & Donev, A. N. (1992). Optimum Experimental Designs.
  • Fedorov, V. V. (1972). Theory of Optimal Experiments.
  • Montgomery, D. C. (2017). Design and Analysis of Experiments.

parse_model_spec

parse_model_spec(search_space: SearchSpace, model_type: Optional[str] = None, effects: Optional[List[str]] = None) -> List[ModelTerm]

Parse user-friendly model specification into internal term representation.

Converts either a model_type shortcut or a list of named effects into a list of :data:ModelTerm tuples that describe each column of the regression (design) matrix.

The intercept term is always included automatically.

Parameters:

Name Type Description Default
search_space SearchSpace

SearchSpace with defined variables.

required
model_type Optional[str]

Shortcut for common model structures. One of:

  • "linear" — Intercept + all main effects.
  • "interaction" — Linear + all pairwise interactions.
  • "quadratic" — Interaction + all squared terms for continuous (real/integer) variables. Categorical variables do not generate quadratic terms.
None
effects Optional[List[str]]

Explicit list of effect strings using variable names. The intercept is added automatically; do not include "1".

Format rules:

  • Main effects: Use the variable name exactly as defined in the SearchSpace.

"Temperature", "Pressure"

  • Interactions: Join two or more variable names with "*".

"Temperature*Pressure", "Catalyst*Temperature"

  • Quadratic terms: Append "**2" to the variable name.

"Temperature**2", "Pressure**2"

Examples:

.. code-block:: python

# Main effects + one interaction + one quadratic
effects=["Temperature", "Pressure", "Catalyst",
         "Temperature*Pressure", "Temperature**2"]

# Main effects only (equivalent to model_type="linear")
effects=["Temperature", "Pressure", "Flow_Rate"]
None

Returns:

Type Description
List[ModelTerm]

List of ModelTerm tuples. Each term is a tuple of

List[ModelTerm]

(variable_index, power) pairs. The intercept is ().

Raises:

Type Description
ValueError

If both or neither of model_type / effects are given, or if an effect references an unknown variable, or if a quadratic term is requested for a categorical variable.

get_model_term_names

get_model_term_names(search_space: SearchSpace, terms: List[ModelTerm]) -> List[str]

Convert internal ModelTerm list to human-readable term names.

Parameters:

Name Type Description Default
search_space SearchSpace

SearchSpace with variable definitions.

required
terms List[ModelTerm]

List of ModelTerm tuples as returned by :func:parse_model_spec.

required

Returns:

Type Description
List[str]

List of strings like ``["Intercept", "Temperature", "Pressure",

List[str]

"Temperature*Pressure", "Temperature^2"]``.

build_column_map

build_column_map(variables: List[Dict[str, Any]]) -> List[Dict[str, Any]]

Coded-column metadata for a variable list.

Continuous (real/integer/discrete) variables occupy one column each; categorical variables occupy one one-hot column per category. Extracted from :func:generate_mixed_candidate_set so the constrained-candidate pipeline can build a column map without generating a full lattice.

Note that context variables produce no column, matching the existing behavior of :func:generate_mixed_candidate_set.

generate_mixed_candidate_set

generate_mixed_candidate_set(search_space: SearchSpace, n_levels: int = 5) -> Tuple[np.ndarray, List[Dict[str, Any]]]

Generate a candidate point grid for mixed continuous/categorical spaces.

For continuous (real/integer) variables, generates n_levels evenly spaced points across the variable bounds. For categorical variables, uses all category values as levels.

The returned candidate array uses coded numeric values:

  • Continuous variables are scaled to [-1, +1].
  • Categorical variables are one-hot encoded (one column per category).

Parameters:

Name Type Description Default
search_space SearchSpace

SearchSpace with defined variables.

required
n_levels int

Number of grid levels for continuous variables (default 5).

5

Returns:

Type Description
Tuple[ndarray, List[Dict[str, Any]]]

Tuple of: - candidates: ndarray of shape (n_candidates, n_coded_columns) with coded numeric values (continuous scaled, categoricals one-hot). - column_map: List of dicts describing each column: {"var_idx": int, "var_name": str, "type": "continuous"|"onehot", "category": str|None}.

build_custom_design_matrix

build_custom_design_matrix(candidates: ndarray, terms: List[ModelTerm], column_map: List[Dict[str, Any]], variables: List[Dict[str, Any]]) -> np.ndarray

Build a regression (design) matrix from candidate points and model terms.

Unlike pyDOE's build_design_matrix(candidates, degree) which generates all polynomial terms up to a given degree, this function builds only the columns corresponding to user-specified model terms.

For continuous variables, columns are computed directly from the coded candidate values. For categorical variables, dummy coding (k-1 columns) is used: the first category is treated as the reference level and absorbed into the intercept. Each remaining category gets one indicator column. This avoids the perfect multicollinearity that full one-hot encoding (k columns) would create with an intercept term.

For example, a categorical variable Catalyst with values ["Pt", "Pd", "Ru"] produces 2 dummy columns (for Pd and Ru), with Pt as the implicit reference level.

Parameters:

Name Type Description Default
candidates ndarray

ndarray of shape (n_candidates, n_coded_columns) as returned by :func:generate_mixed_candidate_set.

required
terms List[ModelTerm]

List of ModelTerm tuples as returned by :func:parse_model_spec.

required
column_map List[Dict[str, Any]]

Column metadata as returned by :func:generate_mixed_candidate_set.

required
variables List[Dict[str, Any]]

List of variable dicts from SearchSpace.variables.

required

Returns:

Type Description
ndarray

Design matrix X of shape (n_candidates, n_model_columns).

decode_candidates

decode_candidates(candidates_coded: ndarray, column_map: List[Dict[str, Any]], variables: List[Dict[str, Any]], selected_indices: Optional[ndarray] = None) -> List[Dict[str, Any]]

Map coded candidate rows back to actual variable values.

Reverses the encoding from :func:generate_mixed_candidate_set:

  • Continuous variables: actual = mid + coded * half_range
  • Categorical variables: decode from one-hot to category name.
  • Integer variables: round to nearest int.

Parameters:

Name Type Description Default
candidates_coded ndarray

Full coded candidate array.

required
column_map List[Dict[str, Any]]

Column metadata from :func:generate_mixed_candidate_set.

required
variables List[Dict[str, Any]]

Variable definitions from SearchSpace.

required
selected_indices Optional[ndarray]

Indices of selected design points. When None, every row of candidates_coded is decoded.

None

Returns:

Type Description
List[Dict[str, Any]]

List of dicts with actual variable values.

encode_candidates

encode_candidates(points: List[Dict[str, Any]], column_map: List[Dict[str, Any]], variables: List[Dict[str, Any]]) -> np.ndarray

Inverse of :func:decode_candidates — raw values back to coded columns.

Continuous variables map to [-1, +1] via (actual - mid) / half_range over the variable's range (discrete uses the min and max of its allowed values). Categorical variables become one-hot columns.

A point must supply a value for every variable in column_map (KeyError otherwise), and a categorical value must equal exactly one of that variable's known categories (ValueError otherwise). Both are deliberate: a candidate set feeding an exchange algorithm must never silently degrade to an all-zero — i.e. no-category-indicated — row.

Parameters:

Name Type Description Default
points List[Dict[str, Any]]

raw-space points, as dicts keyed by variable name. Accepts a list of dicts or anything pandas.DataFrame.to_dict("records") produces.

required
column_map List[Dict[str, Any]]

column metadata from :func:build_column_map.

required
variables List[Dict[str, Any]]

variable dicts from SearchSpace.variables.

required

Returns:

Type Description
ndarray

ndarray of shape (len(points), len(column_map)).

Raises:

Type Description
KeyError

a point is missing a value for a variable in column_map.

ValueError

a categorical value does not match exactly one of the variable's known categories (unknown value, or — see :func:build_column_map — a variable with duplicate category names, which no single raw value can address unambiguously).

run_optimal_design

run_optimal_design(search_space: SearchSpace, n_points: int, model_type: Optional[str] = None, effects: Optional[List[str]] = None, criterion: Literal['D', 'A', 'I'] = 'D', algorithm: Literal['sequential', 'simple_exchange', 'fedorov', 'modified_fedorov', 'detmax'] = 'fedorov', n_levels: int = 5, max_iter: int = 200, random_seed: Optional[int] = None) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]

Generate an optimal experimental design for a user-specified model.

This function creates a statistically efficient set of experimental conditions by optimizing a chosen criterion (D, A, or I) over a candidate grid. Users specify which model terms (main effects, interactions, quadratic terms) they expect to be important, and the algorithm selects the design points that maximize information for estimating those specific terms.

Model Specification:

Provide either model_type (a shortcut) or effects (explicit list), but not both. The intercept is always included automatically.

Effects are specified as a list of strings using variable names from the SearchSpace.

Format rules
  • Main effects: Use the variable name exactly as defined. e.g., "Temperature", "Pressure"
  • Interactions: Join variable names with "*". e.g., "Temperature*Pressure", "Catalyst*Temperature"
  • Quadratic terms: Append "**2" to the variable name. e.g., "Temperature**2", "Pressure**2"

Shortcut model types (via model_type parameter): - "linear": All main effects only. - "interaction": All main effects + all pairwise interactions. - "quadratic": All main effects + all pairwise interactions + all squared terms (continuous variables only).

.. warning::

**Experimental Feature — Categorical Variables**: Optimal design
with categorical variables uses dummy coding (k-1 indicator columns,
first category as reference level) in the design matrix. The
statistical properties of optimal designs with categorical variables
are an active area of research and may not behave identically to
classical continuous-variable optimal designs. Results should be
reviewed carefully. For well-established categorical screening,
consider using Full Factorial or GSD methods instead.

Parameters:

Name Type Description Default
search_space SearchSpace

SearchSpace with defined variables.

required
n_points int

Number of experimental runs to generate.

required
model_type Optional[str]

Shortcut model type. One of "linear", "interaction", "quadratic".

None
effects Optional[List[str]]

Explicit list of effect strings. Examples::

effects=["Temperature", "Pressure", "Temperature*Pressure", "Temperature**2"]

None
criterion Literal['D', 'A', 'I']

Optimality criterion. "D" (default) maximizes parameter information, "A" minimizes average parameter variance, "I" minimizes average prediction variance.

'D'
algorithm Literal['sequential', 'simple_exchange', 'fedorov', 'modified_fedorov', 'detmax']

Optimization algorithm. One of "sequential" (fastest, greedy), "simple_exchange" (Wynn-Mitchell), "fedorov" (recommended default), "modified_fedorov", "detmax" (best quality, slowest).

'fedorov'
n_levels int

Number of grid levels per continuous variable for candidate set generation (default 5). Higher values give finer resolution but increase computation.

5
max_iter int

Maximum iterations for exchange algorithms (default 200).

200
random_seed Optional[int]

Random seed for reproducibility.

None

Returns:

Type Description
Tuple[List[Dict[str, Any]], Dict[str, Any]]

Tuple of: - points: List of dicts with actual variable values. - info: Dict with design quality metrics::

{"criterion": "D", "algorithm": "fedorov",
 "score": 0.042, "D_eff": 89.3, "A_eff": 76.1,
 "p_columns": 6, "n_runs": 15,
 "model_terms": ["Intercept", "Temperature", ...],
 "feasibility": None}

"feasibility" is the info dict returned by :func:alchemist_core.utils.constrained_region.augment_with_boundary (candidate-set provenance counts) when search_space carries input constraints, or None for an unconstrained search space.

Raises:

Type Description
ValueError

If search space has no variables, both/neither model spec args given, unknown criterion/algorithm, or effects reference unknown variables.

Examples:

.. code-block:: python

# Full quadratic model via shortcut
points, info = run_optimal_design(
    search_space, n_points=15, model_type='quadratic',
    criterion='D', algorithm='fedorov',
)

# Custom model: only the effects you expect to matter
points, info = run_optimal_design(
    search_space, n_points=12,
    effects=['Temperature', 'Pressure', 'Catalyst',
             'Temperature*Pressure', 'Temperature**2'],
    criterion='D', algorithm='fedorov',
)

# Screening: main effects + one known interaction
points, info = run_optimal_design(
    search_space, n_points=8,
    effects=['Temperature', 'Pressure', 'Flow_Rate',
             'Temperature*Pressure'],
    criterion='D',
)