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:
Used only when method="optimal". Specify either |
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:
|
None
|
effects
|
Optional[List[str]]
|
Explicit list of effect strings using variable names.
The intercept is added automatically; do not include Format rules:
Examples: .. code-block:: python |
None
|
Returns:
| Type | Description |
|---|---|
List[ModelTerm]
|
List of ModelTerm tuples. Each term is a tuple of |
List[ModelTerm]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both or neither of |
get_model_term_names
¶
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: |
required |
Returns:
| Type | Description |
|---|---|
List[str]
|
List of strings like ``["Intercept", "Temperature", "Pressure", |
List[str]
|
"Temperature*Pressure", "Temperature^2"]``. |
build_column_map
¶
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:
- |
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 |
required |
terms
|
List[ModelTerm]
|
List of ModelTerm tuples as returned by :func: |
required |
column_map
|
List[Dict[str, Any]]
|
Column metadata as returned by
:func: |
required |
variables
|
List[Dict[str, Any]]
|
List of variable dicts from |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Design matrix X of shape |
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: |
required |
variables
|
List[Dict[str, Any]]
|
Variable definitions from SearchSpace. |
required |
selected_indices
|
Optional[ndarray]
|
Indices of selected design points. When |
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 |
required |
column_map
|
List[Dict[str, Any]]
|
column metadata from :func: |
required |
variables
|
List[Dict[str, Any]]
|
variable dicts from |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
ndarray of shape |
Raises:
| Type | Description |
|---|---|
KeyError
|
a point is missing a value for a variable in |
ValueError
|
a categorical value does not match exactly one of the
variable's known categories (unknown value, or — see
:func: |
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 |
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'
|
algorithm
|
Literal['sequential', 'simple_exchange', 'fedorov', 'modified_fedorov', 'detmax']
|
Optimization algorithm. One of |
'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:
-
|
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',
)