doetools 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doetools/__init__.py +44 -0
- doetools/design/__init__.py +28 -0
- doetools/design/d_opt/__init__.py +7 -0
- doetools/design/d_opt/candidate_set.py +185 -0
- doetools/design/d_opt/d_opt.py +299 -0
- doetools/design/d_opt/d_opt_add.py +588 -0
- doetools/design/d_opt/optimizer.py +434 -0
- doetools/design/generic/__init__.py +5 -0
- doetools/design/generic/generic_design.py +219 -0
- doetools/design/mixture/__init__.py +9 -0
- doetools/design/mixture/constrained_mixture.py +240 -0
- doetools/design/mixture/mixture_cp_generator.py +665 -0
- doetools/design/mixture/simplex_centroid.py +184 -0
- doetools/design/mixture/simplex_lattice.py +191 -0
- doetools/design/process/__init__.py +14 -0
- doetools/design/process/box_behnken.py +136 -0
- doetools/design/process/central_composite.py +215 -0
- doetools/design/process/fractional_factorial.py +153 -0
- doetools/design/process/full_factorial.py +104 -0
- doetools/design/process/plackett_burman.py +207 -0
- doetools/graphs/__init__.py +3 -0
- doetools/graphs/data_builder.py +146 -0
- doetools/graphs/design_plot_builder.py +1242 -0
- doetools/graphs/plot_api_mixin.py +1110 -0
- doetools/graphs/renderers.py +3630 -0
- doetools/utils/__init__.py +26 -0
- doetools/utils/abstract_design.py +1085 -0
- doetools/utils/confirmation.py +296 -0
- doetools/utils/design_advisor.py +1360 -0
- doetools/utils/external_points.py +256 -0
- doetools/utils/factors.py +201 -0
- doetools/utils/grid_builder.py +306 -0
- doetools/utils/model_spec.py +375 -0
- doetools/utils/pareto.py +457 -0
- doetools/utils/pdf_report.py +918 -0
- doetools/utils/prediction.py +203 -0
- doetools/utils/regression.py +715 -0
- doetools/utils/summary.py +286 -0
- doetools/utils/upload.py +58 -0
- doetools-0.1.1.dist-info/METADATA +190 -0
- doetools-0.1.1.dist-info/RECORD +44 -0
- doetools-0.1.1.dist-info/WHEEL +5 -0
- doetools-0.1.1.dist-info/licenses/LICENSE +21 -0
- doetools-0.1.1.dist-info/top_level.txt +1 -0
doetools/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Public package interface for :mod:`doetools`."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
|
+
|
|
5
|
+
from .design import (
|
|
6
|
+
BoxBehnkenDesign,
|
|
7
|
+
CentralCompositeDesign,
|
|
8
|
+
ConstrainedMixtureDesign,
|
|
9
|
+
DOptAddDesign,
|
|
10
|
+
DOptDesign,
|
|
11
|
+
FractionalFactorialDesign,
|
|
12
|
+
FullFactorialDesign,
|
|
13
|
+
ImportDesign,
|
|
14
|
+
PlackettBurmanDesign,
|
|
15
|
+
SimplexCentroidDesign,
|
|
16
|
+
SimplexLatticeDesign,
|
|
17
|
+
)
|
|
18
|
+
from .utils import (
|
|
19
|
+
CategoricalFactor,
|
|
20
|
+
ContinuousFactor,
|
|
21
|
+
MixtureFactor,
|
|
22
|
+
ModelTerms,
|
|
23
|
+
suggest_design,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BoxBehnkenDesign",
|
|
28
|
+
"CategoricalFactor",
|
|
29
|
+
"CentralCompositeDesign",
|
|
30
|
+
"ConstrainedMixtureDesign",
|
|
31
|
+
"ContinuousFactor",
|
|
32
|
+
"DOptAddDesign",
|
|
33
|
+
"DOptDesign",
|
|
34
|
+
"FractionalFactorialDesign",
|
|
35
|
+
"FullFactorialDesign",
|
|
36
|
+
"ImportDesign",
|
|
37
|
+
"MixtureFactor",
|
|
38
|
+
"ModelTerms",
|
|
39
|
+
"PlackettBurmanDesign",
|
|
40
|
+
"SimplexCentroidDesign",
|
|
41
|
+
"SimplexLatticeDesign",
|
|
42
|
+
"suggest_design",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .d_opt import DOptAddDesign, DOptDesign
|
|
2
|
+
from .generic import ImportDesign
|
|
3
|
+
from .mixture import (
|
|
4
|
+
ConstrainedMixtureDesign,
|
|
5
|
+
SimplexCentroidDesign,
|
|
6
|
+
SimplexLatticeDesign,
|
|
7
|
+
)
|
|
8
|
+
from .process import (
|
|
9
|
+
BoxBehnkenDesign,
|
|
10
|
+
CentralCompositeDesign,
|
|
11
|
+
FractionalFactorialDesign,
|
|
12
|
+
FullFactorialDesign,
|
|
13
|
+
PlackettBurmanDesign,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BoxBehnkenDesign",
|
|
18
|
+
"CentralCompositeDesign",
|
|
19
|
+
"ConstrainedMixtureDesign",
|
|
20
|
+
"DOptAddDesign",
|
|
21
|
+
"DOptDesign",
|
|
22
|
+
"FractionalFactorialDesign",
|
|
23
|
+
"FullFactorialDesign",
|
|
24
|
+
"ImportDesign",
|
|
25
|
+
"PlackettBurmanDesign",
|
|
26
|
+
"SimplexCentroidDesign",
|
|
27
|
+
"SimplexLatticeDesign"
|
|
28
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Shared candidate-set generation for D-optimal designs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
from collections.abc import Callable, Collection, Mapping
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ...utils.grid_builder import lhs_grid
|
|
11
|
+
from ..mixture.mixture_cp_generator import build_candidate_points
|
|
12
|
+
from ..process import BoxBehnkenDesign, CentralCompositeDesign
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MIXTURE_CANDIDATE_CATEGORIES = (
|
|
16
|
+
"vertices",
|
|
17
|
+
"edge_midpoints",
|
|
18
|
+
"face_centroids",
|
|
19
|
+
"global_centroid",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DOptimalCandidateSetMixin:
|
|
24
|
+
"""Generate process/mixture candidate sets for D-optimal selection."""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def _normalize_mixture_include(
|
|
28
|
+
mixture_include: str | Collection[str] | None,
|
|
29
|
+
) -> tuple[str, ...]:
|
|
30
|
+
if mixture_include is None:
|
|
31
|
+
return ()
|
|
32
|
+
if isinstance(mixture_include, str):
|
|
33
|
+
selected = (
|
|
34
|
+
MIXTURE_CANDIDATE_CATEGORIES
|
|
35
|
+
if mixture_include == "all"
|
|
36
|
+
else (mixture_include,)
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
selected = tuple(mixture_include)
|
|
40
|
+
|
|
41
|
+
unknown = sorted(set(selected) - set(MIXTURE_CANDIDATE_CATEGORIES))
|
|
42
|
+
if unknown:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Unsupported mixture candidate category: "
|
|
45
|
+
+ ", ".join(map(str, unknown))
|
|
46
|
+
+ ". Use 'all' or exact category names: "
|
|
47
|
+
+ ", ".join(MIXTURE_CANDIDATE_CATEGORIES)
|
|
48
|
+
)
|
|
49
|
+
return tuple(dict.fromkeys(selected))
|
|
50
|
+
|
|
51
|
+
def _generate_d_optimal_candidates(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
factors: Mapping[str, object],
|
|
55
|
+
process_strategy: str | None = None,
|
|
56
|
+
mixture_include: str | Collection[str] | None = (),
|
|
57
|
+
mixture_grid: Mapping[str, int] | None = None,
|
|
58
|
+
lhs_n_samples: int | None = None,
|
|
59
|
+
filters: list[Callable[[pd.DataFrame], pd.Series]] | None = None,
|
|
60
|
+
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
61
|
+
"""Return coded and actual candidate matrices for every factor family."""
|
|
62
|
+
factor_names = list(factors)
|
|
63
|
+
continuous = [name for name in factor_names if factors[name].type == "cont"]
|
|
64
|
+
categorical = [name for name in factor_names if factors[name].type == "cat"]
|
|
65
|
+
process = continuous + categorical
|
|
66
|
+
mixture = [name for name in factor_names if factors[name].type == "mix"]
|
|
67
|
+
|
|
68
|
+
include = self._normalize_mixture_include(mixture_include)
|
|
69
|
+
if lhs_n_samples is not None and process_strategy != "lhs":
|
|
70
|
+
raise ValueError("lhs_n_samples can be specified only with 'lhs' strategy")
|
|
71
|
+
if process and process_strategy is None:
|
|
72
|
+
raise ValueError("process_strategy is required when process factors are present")
|
|
73
|
+
if not process and process_strategy is not None:
|
|
74
|
+
raise ValueError("process_strategy cannot be used without process factors")
|
|
75
|
+
if mixture and not include and mixture_grid is None:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"Mixture factors require a non-empty mixture_include or mixture_grid"
|
|
78
|
+
)
|
|
79
|
+
if not mixture and (include or mixture_grid is not None):
|
|
80
|
+
raise ValueError("Mixture candidates were requested without mixture factors")
|
|
81
|
+
|
|
82
|
+
process_coded: pd.DataFrame | None = None
|
|
83
|
+
if process_strategy == "lhs":
|
|
84
|
+
if lhs_n_samples is None:
|
|
85
|
+
raise ValueError("lhs_n_samples must be specified for 'lhs' strategy")
|
|
86
|
+
if lhs_n_samples <= 0:
|
|
87
|
+
raise ValueError("lhs_n_samples must be greater than zero")
|
|
88
|
+
if not continuous:
|
|
89
|
+
raise ValueError("'lhs' strategy requires at least one continuous factor")
|
|
90
|
+
process_coded = lhs_grid(continuous, lhs_n_samples, random_state=42)
|
|
91
|
+
process_coded = self._cross_categorical(
|
|
92
|
+
process_coded, factors, categorical
|
|
93
|
+
)
|
|
94
|
+
elif process_strategy == "grid":
|
|
95
|
+
levels = [factors[name].coded_levels for name in process]
|
|
96
|
+
process_coded = pd.DataFrame(
|
|
97
|
+
itertools.product(*levels), columns=process
|
|
98
|
+
)
|
|
99
|
+
elif process_strategy in {"ccc", "ccf", "cci"}:
|
|
100
|
+
if not continuous:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"'{process_strategy}' strategy requires continuous factors"
|
|
103
|
+
)
|
|
104
|
+
ccd = CentralCompositeDesign(
|
|
105
|
+
factors={name: factors[name] for name in continuous},
|
|
106
|
+
design=process_strategy,
|
|
107
|
+
center_points=1,
|
|
108
|
+
replicates=0,
|
|
109
|
+
)
|
|
110
|
+
process_coded = self._cross_categorical(
|
|
111
|
+
ccd._coded_design_matrix, factors, categorical
|
|
112
|
+
)
|
|
113
|
+
elif process_strategy == "bb":
|
|
114
|
+
if not continuous:
|
|
115
|
+
raise ValueError("'bb' strategy requires continuous factors")
|
|
116
|
+
bb = BoxBehnkenDesign(
|
|
117
|
+
factors={name: factors[name] for name in continuous},
|
|
118
|
+
replicates=0,
|
|
119
|
+
center_points=1,
|
|
120
|
+
)
|
|
121
|
+
process_coded = self._cross_categorical(
|
|
122
|
+
bb._coded_design_matrix, factors, categorical
|
|
123
|
+
)
|
|
124
|
+
elif process_strategy is not None:
|
|
125
|
+
raise ValueError(f"Unknown process design strategy: {process_strategy!r}")
|
|
126
|
+
|
|
127
|
+
process_actual: pd.DataFrame | None = None
|
|
128
|
+
if process_coded is not None:
|
|
129
|
+
process_actual = self._decode_matrix(process_coded)
|
|
130
|
+
for name in continuous:
|
|
131
|
+
process_actual[name] = process_actual[name].round(
|
|
132
|
+
factors[name].decimals
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
mixture_actual: pd.DataFrame | None = None
|
|
136
|
+
if mixture:
|
|
137
|
+
mixture_actual = build_candidate_points(
|
|
138
|
+
lower_bounds=[factors[name].lower_bound for name in mixture],
|
|
139
|
+
upper_bounds=[factors[name].upper_bound for name in mixture],
|
|
140
|
+
component_names=mixture,
|
|
141
|
+
include=include,
|
|
142
|
+
grid=mixture_grid,
|
|
143
|
+
)[mixture].reset_index(drop=True)
|
|
144
|
+
|
|
145
|
+
if process_actual is not None and mixture_actual is not None:
|
|
146
|
+
candidates = process_actual.merge(mixture_actual, how="cross")
|
|
147
|
+
elif process_actual is not None:
|
|
148
|
+
candidates = process_actual.copy()
|
|
149
|
+
elif mixture_actual is not None:
|
|
150
|
+
candidates = mixture_actual.copy()
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError("No candidate points requested")
|
|
153
|
+
|
|
154
|
+
if filters:
|
|
155
|
+
mask = pd.Series(True, index=candidates.index)
|
|
156
|
+
for domain_filter in filters:
|
|
157
|
+
result = domain_filter(candidates)
|
|
158
|
+
if not isinstance(result, pd.Series) or len(result) != len(candidates):
|
|
159
|
+
raise TypeError(
|
|
160
|
+
"Each filter must return a pandas Series with one value per candidate"
|
|
161
|
+
)
|
|
162
|
+
mask &= result.astype(bool)
|
|
163
|
+
candidates = candidates.loc[mask]
|
|
164
|
+
|
|
165
|
+
candidates = candidates[factor_names].drop_duplicates().reset_index(drop=True)
|
|
166
|
+
if candidates.empty:
|
|
167
|
+
raise ValueError("Candidate-point generation produced an empty set")
|
|
168
|
+
coded = self._code_matrix(candidates)[factor_names].reset_index(drop=True)
|
|
169
|
+
return coded, candidates
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _cross_categorical(
|
|
173
|
+
process_coded: pd.DataFrame,
|
|
174
|
+
factors: Mapping[str, object],
|
|
175
|
+
categorical: list[str],
|
|
176
|
+
) -> pd.DataFrame:
|
|
177
|
+
if not categorical:
|
|
178
|
+
return process_coded.reset_index(drop=True)
|
|
179
|
+
category_points = pd.DataFrame(
|
|
180
|
+
itertools.product(
|
|
181
|
+
*(factors[name].coded_levels for name in categorical)
|
|
182
|
+
),
|
|
183
|
+
columns=categorical,
|
|
184
|
+
)
|
|
185
|
+
return process_coded.merge(category_points, how="cross")
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""D-optimal experimental designs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Collection, Mapping
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import plotly.graph_objects as go
|
|
11
|
+
|
|
12
|
+
from ...graphs import GraphsMixin
|
|
13
|
+
from ...graphs.design_plot_builder import DesignPlotOptions, build_design_plot
|
|
14
|
+
from ...utils import Design
|
|
15
|
+
from ...utils.model_spec import ModelTerms, compile_model_spec
|
|
16
|
+
from .candidate_set import DOptimalCandidateSetMixin
|
|
17
|
+
from .optimizer import (
|
|
18
|
+
build_d_optimal_solutions_figure,
|
|
19
|
+
optimize_d_optimal,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DOptDesign(DOptimalCandidateSetMixin, Design, GraphsMixin):
|
|
24
|
+
"""Generate and select a D-optimal experimental design.
|
|
25
|
+
|
|
26
|
+
Candidate points are generated during initialization. Process candidates
|
|
27
|
+
come from ``process_strategy``; mixture candidates are explicitly selected
|
|
28
|
+
with ``mixture_include``, ``mixture_grid``, or both. For mixed designs, the
|
|
29
|
+
process and mixture candidate sets are combined by Cartesian product.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
factors (dict[str, object]): Mapping of names to ``ContinuousFactor``,
|
|
33
|
+
``CategoricalFactor``, or ``MixtureFactor`` objects.
|
|
34
|
+
process_strategy (str, optional): Candidate strategy for process factors.
|
|
35
|
+
Supported values are ``"grid"``, ``"lhs"``, ``"ccc"``, ``"ccf"``,
|
|
36
|
+
``"cci"``, and ``"bb"``. Required when process factors are present
|
|
37
|
+
and omitted for mixture-only designs. Defaults to ``None``.
|
|
38
|
+
mixture_include (str | Collection[str] | None, optional): Geometric
|
|
39
|
+
mixture candidates. Pass ``"all"`` or select from ``"vertices"``,
|
|
40
|
+
``"edge_midpoints"``, ``"face_centroids"``, and
|
|
41
|
+
``"global_centroid"``. Defaults to an empty collection.
|
|
42
|
+
mixture_grid (Mapping[str, int] | None, optional): Optional bounded
|
|
43
|
+
simplex-lattice configuration. The mapping must contain ``"degree"``
|
|
44
|
+
and may contain ``"max_candidates"``. Defaults to ``None``.
|
|
45
|
+
lhs_n_samples (int | None, optional): Number of Latin-hypercube points.
|
|
46
|
+
Required only when ``process_strategy="lhs"``. Defaults to ``None``.
|
|
47
|
+
filters (list[Callable] | None, optional): Functions evaluated on the
|
|
48
|
+
actual-valued candidate frame. Each function must return one Boolean
|
|
49
|
+
value per candidate row. Defaults to ``None``.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If ``factors`` is empty or candidate-generation options are
|
|
53
|
+
missing, incompatible, or produce an empty candidate set.
|
|
54
|
+
TypeError: If a candidate filter does not return a pandas Series with one
|
|
55
|
+
value per candidate row.
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
Mixture geometry is opt-in. A design containing mixture factors must
|
|
59
|
+
request ``mixture_include``, ``mixture_grid``, or both.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
factors: dict[str, object],
|
|
65
|
+
process_strategy: Literal["grid", "ccf", "cci", "ccc", "bb", "lhs"]
|
|
66
|
+
| None = None,
|
|
67
|
+
mixture_include: Literal[
|
|
68
|
+
"all",
|
|
69
|
+
"vertices",
|
|
70
|
+
"edge_midpoints",
|
|
71
|
+
"face_centroids",
|
|
72
|
+
"global_centroid",
|
|
73
|
+
]
|
|
74
|
+
| Collection[str]
|
|
75
|
+
| None = (),
|
|
76
|
+
mixture_grid: Mapping[str, int] | None = None,
|
|
77
|
+
lhs_n_samples: int | None = None,
|
|
78
|
+
filters: list[Callable[[pd.DataFrame], pd.Series]] | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__()
|
|
81
|
+
if not factors:
|
|
82
|
+
raise ValueError("factors must contain at least one factor")
|
|
83
|
+
self._factors = factors
|
|
84
|
+
self.set_domain_filters(filters)
|
|
85
|
+
self._coded_cp, self._cp = self.build_cp_matrix(
|
|
86
|
+
factors=factors,
|
|
87
|
+
process_strategy=process_strategy,
|
|
88
|
+
mixture_include=mixture_include,
|
|
89
|
+
mixture_grid=mixture_grid,
|
|
90
|
+
lhs_n_samples=lhs_n_samples,
|
|
91
|
+
filters=filters,
|
|
92
|
+
)
|
|
93
|
+
self._cp_model_matrix: pd.DataFrame | None = None
|
|
94
|
+
self._best_idx: dict[int, list[int]] = {}
|
|
95
|
+
self._log_det: pd.DataFrame | None = None
|
|
96
|
+
self._design_type = "D-Optimal"
|
|
97
|
+
|
|
98
|
+
def build_cp_matrix(
|
|
99
|
+
self,
|
|
100
|
+
factors: dict[str, object],
|
|
101
|
+
process_strategy: str | None = None,
|
|
102
|
+
mixture_include: str | Collection[str] | None = (),
|
|
103
|
+
mixture_grid: Mapping[str, int] | None = None,
|
|
104
|
+
lhs_n_samples: int | None = None,
|
|
105
|
+
filters: list[Callable[[pd.DataFrame], pd.Series]] | None = None,
|
|
106
|
+
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
107
|
+
"""Build and return coded and actual D-optimal candidate matrices."""
|
|
108
|
+
return self._generate_d_optimal_candidates(
|
|
109
|
+
factors=factors,
|
|
110
|
+
process_strategy=process_strategy,
|
|
111
|
+
mixture_include=mixture_include,
|
|
112
|
+
mixture_grid=mixture_grid,
|
|
113
|
+
lhs_n_samples=lhs_n_samples,
|
|
114
|
+
filters=filters,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def set_model_terms(self, terms: ModelTerms = ModelTerms) -> None:
|
|
118
|
+
"""Define the model optimized by the D-optimal search.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
terms (ModelTerms): Process and mixture terms to include.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If the model contains more coefficients than candidate
|
|
125
|
+
points.
|
|
126
|
+
"""
|
|
127
|
+
process = [
|
|
128
|
+
name
|
|
129
|
+
for name, factor in self._factors.items()
|
|
130
|
+
if factor.type in {"cont", "cat"}
|
|
131
|
+
]
|
|
132
|
+
mixture = [
|
|
133
|
+
name for name, factor in self._factors.items() if factor.type == "mix"
|
|
134
|
+
]
|
|
135
|
+
self._model_spec = compile_model_spec(terms, process, mixture)
|
|
136
|
+
if self._model_spec.model_terms > len(self._cp):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"The number of model terms ({self._model_spec.model_terms}) "
|
|
139
|
+
f"exceeds the number of candidate points ({len(self._cp)})."
|
|
140
|
+
)
|
|
141
|
+
self._cp_model_matrix = self._build_model_matrix(
|
|
142
|
+
self._coded_cp, self._model_spec
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def compute_d_optimal(
|
|
146
|
+
self,
|
|
147
|
+
n_min: int,
|
|
148
|
+
n_max: int,
|
|
149
|
+
step: int = 1,
|
|
150
|
+
trials: int = 50,
|
|
151
|
+
max_no_improve: int = 5,
|
|
152
|
+
graph: bool = True,
|
|
153
|
+
random_state: int | np.random.Generator | None = None,
|
|
154
|
+
) -> go.Figure:
|
|
155
|
+
"""Compute D-optimal subsets for the requested total run counts.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
n_min (int): Smallest total run count to optimize.
|
|
159
|
+
n_max (int): Largest total run count to optimize.
|
|
160
|
+
step (int, optional): Increment between optimized run counts.
|
|
161
|
+
Defaults to 1.
|
|
162
|
+
trials (int, optional): Random exchange-search starts per run count.
|
|
163
|
+
Defaults to 50.
|
|
164
|
+
max_no_improve (int, optional): Maximum rejected improving-exchange
|
|
165
|
+
proposals before a trial stops. Defaults to 5.
|
|
166
|
+
graph (bool, optional): Retained for API compatibility. The returned
|
|
167
|
+
figure is never displayed automatically. Defaults to ``True``.
|
|
168
|
+
random_state (int | numpy.random.Generator | None, optional): Seed or
|
|
169
|
+
generator used by the randomized starts. Defaults to ``None``.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
plotly.graph_objects.Figure: D-optimality curve for the computed run
|
|
173
|
+
counts.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: If model terms have not been set or the requested run
|
|
177
|
+
range is invalid for the candidate set and model.
|
|
178
|
+
RuntimeError: If a full-rank subset cannot be found.
|
|
179
|
+
"""
|
|
180
|
+
if self._cp_model_matrix is None:
|
|
181
|
+
raise ValueError("No model matrix defined; call 'set_model_terms' first")
|
|
182
|
+
result = optimize_d_optimal(
|
|
183
|
+
self._cp_model_matrix,
|
|
184
|
+
n_min=n_min,
|
|
185
|
+
n_max=n_max,
|
|
186
|
+
step=step,
|
|
187
|
+
trials=trials,
|
|
188
|
+
max_no_improve=max_no_improve,
|
|
189
|
+
random_state=random_state,
|
|
190
|
+
)
|
|
191
|
+
self._best_idx = result.best_indices
|
|
192
|
+
self._log_det = result.log_det
|
|
193
|
+
return build_d_optimal_solutions_figure(self._log_det)
|
|
194
|
+
|
|
195
|
+
def plot_candidate_set(
|
|
196
|
+
self,
|
|
197
|
+
ax1: str,
|
|
198
|
+
ax2: str,
|
|
199
|
+
ax3: str | None = None,
|
|
200
|
+
ax4: str | None = None,
|
|
201
|
+
coded: bool = False,
|
|
202
|
+
*,
|
|
203
|
+
show_title: bool = True,
|
|
204
|
+
show_summary: bool = True,
|
|
205
|
+
show_legend: bool = True,
|
|
206
|
+
show_grid: bool = True,
|
|
207
|
+
marker_color: str = "#495057",
|
|
208
|
+
show_hover: bool = True,
|
|
209
|
+
show_run_labels: bool = False,
|
|
210
|
+
show_replicate_count: bool = True,
|
|
211
|
+
aggregate_projected_points: bool = True,
|
|
212
|
+
axis_label_mode: str = "symbol_unit",
|
|
213
|
+
height: int | None = None,
|
|
214
|
+
domain: Literal["full", "allowed"] = "full",
|
|
215
|
+
) -> go.Figure:
|
|
216
|
+
"""Visualize the candidate set before selecting a design.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
ax1 (str): First factor shown in the plot.
|
|
220
|
+
ax2 (str): Second factor shown in the plot.
|
|
221
|
+
ax3 (str, optional): Third plotted factor.
|
|
222
|
+
ax4 (str, optional): Fourth mixture component for a tetrahedral
|
|
223
|
+
plot.
|
|
224
|
+
coded (bool): Whether to display coded rather than actual units.
|
|
225
|
+
show_title (bool): Whether to display the generated title.
|
|
226
|
+
show_summary (bool): Whether to display the candidate-set summary.
|
|
227
|
+
show_legend (bool): Whether to display the legend.
|
|
228
|
+
show_grid (bool): Whether to display grid lines.
|
|
229
|
+
marker_color (str): Plotly-compatible marker color.
|
|
230
|
+
show_hover (bool): Whether to display point information on hover.
|
|
231
|
+
show_run_labels (bool): Whether to label candidate points.
|
|
232
|
+
show_replicate_count (bool): Whether to include counts for repeated
|
|
233
|
+
candidate settings.
|
|
234
|
+
aggregate_projected_points (bool): Whether candidates with identical
|
|
235
|
+
plotted coordinates are combined when other factors are omitted.
|
|
236
|
+
axis_label_mode (str): Axis-label format. Supported values are
|
|
237
|
+
``"symbol_unit"``, ``"symbol"``, and ``"name"``.
|
|
238
|
+
height (int, optional): Figure height in pixels.
|
|
239
|
+
domain (str): Mixture domain to draw. ``"full"`` draws the full
|
|
240
|
+
simplex or tetrahedron; ``"allowed"`` draws the domain covered
|
|
241
|
+
by the candidate points.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
plotly.graph_objects.Figure: Candidate-set visualization.
|
|
245
|
+
|
|
246
|
+
Notes:
|
|
247
|
+
Two or three non-mixture factors produce Cartesian plots. Two,
|
|
248
|
+
three, or four mixture components produce mixture-line, ternary, or
|
|
249
|
+
tetrahedral plots.
|
|
250
|
+
"""
|
|
251
|
+
options = DesignPlotOptions(
|
|
252
|
+
coded=coded,
|
|
253
|
+
marker_color=marker_color,
|
|
254
|
+
show_title=show_title,
|
|
255
|
+
show_summary=show_summary,
|
|
256
|
+
show_legend=show_legend,
|
|
257
|
+
show_grid=show_grid,
|
|
258
|
+
show_hover=show_hover,
|
|
259
|
+
show_run_labels=show_run_labels,
|
|
260
|
+
show_replicate_count=show_replicate_count,
|
|
261
|
+
aggregate_projected_points=aggregate_projected_points,
|
|
262
|
+
axis_label_mode=axis_label_mode,
|
|
263
|
+
height=height,
|
|
264
|
+
domain=domain,
|
|
265
|
+
)
|
|
266
|
+
return build_design_plot(
|
|
267
|
+
design_matrix=self._cp,
|
|
268
|
+
coded_design_matrix=self._coded_cp,
|
|
269
|
+
factors=self._factors,
|
|
270
|
+
design_type="Candidate Set Design",
|
|
271
|
+
axes=[axis for axis in (ax1, ax2, ax3, ax4) if axis is not None],
|
|
272
|
+
options=options,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def select_design(self, n: int) -> None:
|
|
276
|
+
"""Select a previously computed design containing ``n`` total runs.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
n (int): Total run count returned by ``compute_d_optimal``.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
KeyError: If no solution was computed for ``n``.
|
|
283
|
+
"""
|
|
284
|
+
if n not in self._best_idx:
|
|
285
|
+
raise KeyError(
|
|
286
|
+
f"No D-optimal solution was computed for {n} runs"
|
|
287
|
+
)
|
|
288
|
+
indices = self._best_idx[n]
|
|
289
|
+
self._coded_design_matrix = self._coded_cp.iloc[indices].reset_index(drop=True)
|
|
290
|
+
self._design_matrix = self._cp.iloc[indices].reset_index(drop=True)
|
|
291
|
+
self._model_matrix = self._build_model_matrix(
|
|
292
|
+
self._coded_design_matrix, self._model_spec
|
|
293
|
+
)
|
|
294
|
+
self._number_of_center_points(self._coded_design_matrix)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def log_det(self) -> pd.DataFrame | None:
|
|
298
|
+
"""Normalized log-determinant values indexed by total run count."""
|
|
299
|
+
return self._log_det
|