- 310 registry:skill items, 1528 files, 7 categories (hub tags) - registry.json generated by @kortix/registry buildRegistry - nested SKILL.md in sn-da-* renamed SUBSKILL.md (no phantom skills) - provenance + licenses: SOURCES.md
184 lines
7.6 KiB
Python
184 lines
7.6 KiB
Python
"""Design-of-experiments (DOE) matrices as labeled, decoded pandas DataFrames.
|
|
|
|
pyDOE3 returns designs in *coded* units (-1/+1, or 0..k-1). Researchers want the
|
|
design in *real* factor units (temperature in C, concentration in mM) with named
|
|
columns, randomized run order, and a clear sense of what each design is for. This
|
|
module wraps pyDOE3 to do exactly that.
|
|
|
|
A `factors` spec maps factor names to their real-world levels:
|
|
- two-level / continuous: {"temp": (20, 60), "conc": (1, 10)} # (low, high)
|
|
- multi-level categorical: {"catalyst": ["A", "B", "C"]}
|
|
|
|
Functions:
|
|
full_factorial every combination of given levels (cost grows fast)
|
|
two_level_factorial 2^k full factorial (screening + interactions)
|
|
fractional_factorial 2^(k-p) fraction (screening many factors cheaply)
|
|
plackett_burman very economical main-effects-only screening
|
|
central_composite response-surface design (curvature / optimization)
|
|
box_behnken response-surface design, no extreme corners
|
|
latin_hypercube space-filling sample for simulation / computer experiments
|
|
|
|
Each returns a DataFrame in real units; pass randomize=True (default) to also get
|
|
a randomized 'run_order'. Requires: pyDOE3, numpy, pandas.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
def _decode_two_level(coded, factors):
|
|
"""Map a -1/+1 coded matrix to real (low/high) units per factor."""
|
|
names = list(factors)
|
|
out = {}
|
|
for j, name in enumerate(names):
|
|
lvl = factors[name]
|
|
low, high = lvl[0], lvl[1]
|
|
mid, half = (high + low) / 2.0, (high - low) / 2.0
|
|
out[name] = mid + coded[:, j] * half
|
|
return pd.DataFrame(out)
|
|
|
|
|
|
def _randomize(df, randomize, seed):
|
|
if not randomize:
|
|
return df.reset_index(drop=True)
|
|
rng = np.random.default_rng(seed)
|
|
order = rng.permutation(len(df)) + 1
|
|
df = df.copy()
|
|
df.insert(0, "run_order", order)
|
|
return df.sort_values("run_order").reset_index(drop=True)
|
|
|
|
|
|
def full_factorial(factors, randomize=True, seed=0):
|
|
"""Every combination of the listed levels.
|
|
|
|
factors values are explicit level lists, e.g.
|
|
{"temp": [20, 40, 60], "catalyst": ["A", "B"]} -> 3*2 = 6 runs.
|
|
Runs = product of level counts, so this explodes quickly with many factors.
|
|
"""
|
|
from pyDOE3 import fullfact
|
|
names = list(factors)
|
|
levels = [list(factors[n]) for n in names]
|
|
counts = [len(l) for l in levels]
|
|
coded = fullfact(counts).astype(int)
|
|
data = {n: [levels[j][coded[i, j]] for i in range(len(coded))]
|
|
for j, n in enumerate(names)}
|
|
return _randomize(pd.DataFrame(data), randomize, seed)
|
|
|
|
|
|
def two_level_factorial(factors, randomize=True, seed=0):
|
|
"""Full 2^k factorial: all main effects and all interactions, estimable.
|
|
|
|
Each factor needs a (low, high) pair. Use for k up to ~5; beyond that the
|
|
run count (2^k) gets expensive — switch to fractional_factorial or
|
|
plackett_burman for screening.
|
|
"""
|
|
from pyDOE3 import ff2n
|
|
coded = ff2n(len(factors))
|
|
return _randomize(_decode_two_level(coded, factors), randomize, seed)
|
|
|
|
|
|
def fractional_factorial(factors, generator, randomize=True, seed=0):
|
|
"""2^(k-p) fractional factorial from a generator string.
|
|
|
|
`generator` is pyDOE3's Yates notation, e.g. for 4 factors in 8 runs (one of
|
|
them aliased): "a b c abc". Each token defines a column; multi-letter tokens
|
|
alias a factor with an interaction (this is the tradeoff — fewer runs, some
|
|
effects confounded). Choose a higher-resolution generator if you need to
|
|
separate main effects from two-factor interactions.
|
|
"""
|
|
from pyDOE3 import fracfact
|
|
coded = fracfact(generator)
|
|
if coded.shape[1] != len(factors):
|
|
raise ValueError(f"generator defines {coded.shape[1]} factors but "
|
|
f"{len(factors)} were named")
|
|
return _randomize(_decode_two_level(coded, factors), randomize, seed)
|
|
|
|
|
|
def plackett_burman(factors, randomize=True, seed=0):
|
|
"""Plackett-Burman screening design: main effects only, very few runs.
|
|
|
|
Ideal for screening many factors (run count is the next multiple of 4 above k)
|
|
to find the vital few. Two-factor interactions are heavily confounded with main
|
|
effects, so use it to screen, not to model interactions.
|
|
"""
|
|
from pyDOE3 import pbdesign
|
|
coded = pbdesign(len(factors)) # may include extra dummy columns
|
|
coded = coded[:, :len(factors)]
|
|
return _randomize(_decode_two_level(coded, factors), randomize, seed)
|
|
|
|
|
|
def central_composite(factors, center=(0, 1), alpha="orthogonal",
|
|
face="circumscribed", randomize=True, seed=0):
|
|
"""Central composite design (CCD) for response-surface / optimization work.
|
|
|
|
Adds axial ("star") points and center points to a 2^k factorial so you can fit
|
|
a quadratic model and locate an optimum. With face='circumscribed' the axial
|
|
points sit OUTSIDE the (low, high) box (so real levels exceed your stated
|
|
range); use face='inscribed' or 'faced' to keep everything within range.
|
|
`center` = (n center pts in factorial block, n in axial block).
|
|
"""
|
|
from pyDOE3 import ccdesign
|
|
coded = ccdesign(len(factors), center=center, alpha=alpha, face=face)
|
|
return _randomize(_decode_two_level(coded, factors), randomize, seed)
|
|
|
|
|
|
def box_behnken(factors, center=1, randomize=True, seed=0):
|
|
"""Box-Behnken response-surface design (needs >= 3 factors).
|
|
|
|
Like a CCD it fits a quadratic, but it never uses the extreme corner
|
|
combinations (all-low or all-high), which is useful when those corners are
|
|
unsafe or infeasible. More economical than a CCD for 3-5 factors.
|
|
"""
|
|
from pyDOE3 import bbdesign
|
|
if len(factors) < 3:
|
|
raise ValueError("box_behnken requires at least 3 factors")
|
|
coded = bbdesign(len(factors), center=center)
|
|
return _randomize(_decode_two_level(coded, factors), randomize, seed)
|
|
|
|
|
|
def latin_hypercube(factors, n_samples, criterion="maximin", seed=0,
|
|
randomize=False):
|
|
"""Space-filling Latin hypercube sample over continuous factor ranges.
|
|
|
|
For computer experiments / simulations where you want even coverage of a
|
|
high-dimensional space with relatively few points. Each factor needs a
|
|
(low, high) range. `criterion`: 'maximin' spreads points apart;
|
|
'center'/'centermaximin'/'correlation' are alternatives.
|
|
"""
|
|
from pyDOE3 import lhs
|
|
# pyDOE3 draws from its own default_rng, so seeding numpy's global RNG has
|
|
# no effect -- the seed has to be handed to lhs itself.
|
|
names = list(factors)
|
|
unit = lhs( # in [0,1]
|
|
len(names), samples=n_samples, criterion=criterion, seed=int(seed)
|
|
)
|
|
out = {}
|
|
for j, n in enumerate(names):
|
|
low, high = factors[n][0], factors[n][1]
|
|
out[n] = low + unit[:, j] * (high - low)
|
|
df = pd.DataFrame(out)
|
|
return _randomize(df, randomize, seed)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
f2 = {"temp": (20, 60), "conc": (1, 10), "ph": (6, 8)}
|
|
|
|
print("== 2^3 full factorial ==")
|
|
print(two_level_factorial(f2, seed=1).to_string(index=False))
|
|
|
|
print("\n== fractional 2^(4-1), generator 'a b c abc' ==")
|
|
f4 = {"A": (-1, 1), "B": (-1, 1), "C": (-1, 1), "D": (-1, 1)}
|
|
print(fractional_factorial(f4, "a b c abc", seed=1).to_string(index=False))
|
|
|
|
print("\n== Plackett-Burman screening, 5 factors ==")
|
|
f5 = {f"x{i}": (0, 1) for i in range(1, 6)}
|
|
print(f"runs = {len(plackett_burman(f5, randomize=False))}")
|
|
|
|
print("\n== central composite (2 factors) ==")
|
|
print(central_composite({"temp": (20, 60), "conc": (1, 10)}, seed=1).round(2).to_string(index=False))
|
|
|
|
print("\n== Latin hypercube, 8 samples over 3 factors ==")
|
|
print(latin_hypercube(f2, 8, seed=1).round(2).to_string(index=False))
|