discopt-doe 0.2.0__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.
- discopt/doe/__init__.py +282 -0
- discopt/doe/acquisition.py +221 -0
- discopt/doe/anova.py +399 -0
- discopt/doe/cli.py +2220 -0
- discopt/doe/design.py +1168 -0
- discopt/doe/discrimination.py +840 -0
- discopt/doe/discrimination_sequential.py +243 -0
- discopt/doe/estimability.py +394 -0
- discopt/doe/exploration.py +225 -0
- discopt/doe/fim.py +953 -0
- discopt/doe/fractional.py +363 -0
- discopt/doe/gui/__init__.py +6 -0
- discopt/doe/gui/app.py +2342 -0
- discopt/doe/gui/discopt-logo.png +0 -0
- discopt/doe/gui/launcher.py +142 -0
- discopt/doe/install.py +71 -0
- discopt/doe/latin.py +405 -0
- discopt/doe/model_based.py +549 -0
- discopt/doe/optimize.py +366 -0
- discopt/doe/profile.py +426 -0
- discopt/doe/screening.py +352 -0
- discopt/doe/selection.py +391 -0
- discopt/doe/sequential.py +225 -0
- discopt/doe/skill/SKILL.md +169 -0
- discopt/doe/skill/agents/doe-expert.md +83 -0
- discopt/doe/skill/agents/estimability-expert.md +125 -0
- discopt/doe/skill/agents/identifiability-expert.md +111 -0
- discopt/doe/skill/agents/model-discrimination-expert.md +138 -0
- discopt/doe/surrogate.py +299 -0
- discopt/doe/templates.py +623 -0
- discopt/doe/workbook.py +794 -0
- discopt_doe-0.2.0.dist-info/METADATA +203 -0
- discopt_doe-0.2.0.dist-info/RECORD +36 -0
- discopt_doe-0.2.0.dist-info/WHEEL +5 -0
- discopt_doe-0.2.0.dist-info/entry_points.txt +5 -0
- discopt_doe-0.2.0.dist-info/top_level.txt +1 -0
discopt/doe/__init__.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""discopt.doe -- Design of Experiments.
|
|
2
|
+
|
|
3
|
+
This package has three complementary entry points, each tailored to
|
|
4
|
+
a different question:
|
|
5
|
+
|
|
6
|
+
1. **"What is the best operating condition?"**
|
|
7
|
+
:func:`optimize_round` runs one active-learning round: fit a
|
|
8
|
+
surrogate to the experiments completed so far, recommend the
|
|
9
|
+
next batch via an acquisition function (expected improvement,
|
|
10
|
+
UCB, steepest ascent), append the batch to a workbook for
|
|
11
|
+
execution. See the ``active-learning`` tutorial notebook.
|
|
12
|
+
|
|
13
|
+
2. **"Does this factor matter?"**
|
|
14
|
+
:func:`factorial_2level_design` builds 2-level full factorial
|
|
15
|
+
screening designs; :func:`effects_estimates` gives signed
|
|
16
|
+
main-effect estimates; :func:`anova_report` produces the
|
|
17
|
+
F-table.
|
|
18
|
+
|
|
19
|
+
3. **"How precisely can I estimate the model parameters?"**
|
|
20
|
+
:func:`optimal_experiment` and :func:`batch_optimal_experiment`
|
|
21
|
+
solve for an exact D/A/E-optimal design using the Fisher
|
|
22
|
+
Information Matrix computed with JAX autodiff;
|
|
23
|
+
:func:`diagnose_identifiability` and
|
|
24
|
+
:func:`estimability_rank` warn when the chosen experiments
|
|
25
|
+
cannot identify the parameters.
|
|
26
|
+
|
|
27
|
+
Quick start (FIM-based parameter-estimation design)
|
|
28
|
+
---------------------------------------------------
|
|
29
|
+
>>> from discopt.doe import compute_fim, optimal_experiment, DesignCriterion
|
|
30
|
+
>>> fim_result = compute_fim(experiment, param_values, design_values)
|
|
31
|
+
>>> design = optimal_experiment(experiment, param_values, design_bounds)
|
|
32
|
+
>>> print(design.summary())
|
|
33
|
+
|
|
34
|
+
Quick start (active-learning optimization)
|
|
35
|
+
------------------------------------------
|
|
36
|
+
>>> from discopt.doe import optimize_round, OptimizationCriterion
|
|
37
|
+
>>> result = optimize_round(
|
|
38
|
+
... workbook="opt.xlsx",
|
|
39
|
+
... criterion=OptimizationCriterion.MAXIMIZE,
|
|
40
|
+
... surrogate="gp", # or sklearn estimator, or Surrogate
|
|
41
|
+
... acquisition="expected_improvement",
|
|
42
|
+
... batch_size=4,
|
|
43
|
+
... )
|
|
44
|
+
>>> print(result.next_designs)
|
|
45
|
+
|
|
46
|
+
Identifiability and estimability diagnostics
|
|
47
|
+
--------------------------------------------
|
|
48
|
+
>>> from discopt.doe import diagnose_identifiability, estimability_rank, profile_likelihood
|
|
49
|
+
>>> diag = diagnose_identifiability(experiment, param_values)
|
|
50
|
+
>>> est = estimability_rank(experiment, param_values)
|
|
51
|
+
>>> profile = profile_likelihood(experiment, data, "k")
|
|
52
|
+
|
|
53
|
+
See Also
|
|
54
|
+
--------
|
|
55
|
+
discopt.estimate : Parameter estimation using the same Experiment interface.
|
|
56
|
+
discopt.doe.surrogate : Surrogate model protocol + sklearn adapter.
|
|
57
|
+
discopt.doe.acquisition : Acquisition functions (EI, UCB, steepest ascent).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from importlib.metadata import PackageNotFoundError as _PkgNotFound
|
|
61
|
+
from importlib.metadata import version as _dist_version
|
|
62
|
+
|
|
63
|
+
# discopt-doe consumes the public discopt.parametric API and the
|
|
64
|
+
# "discopt.cli" plugin hook, both introduced in discopt 0.6. A stale base
|
|
65
|
+
# install fails here with a clear message instead of an AttributeError later.
|
|
66
|
+
# Two-component compare so 0.6.0.dev0 local builds pass.
|
|
67
|
+
_MIN_DISCOPT = (0, 6)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _version_prefix(found: str) -> tuple[int, ...]:
|
|
71
|
+
"""(major, minor) integer prefix, tolerant of pre-release suffixes.
|
|
72
|
+
|
|
73
|
+
``"0.6rc1"`` -> ``(0, 6)``, ``"0.7b2"`` -> ``(0, 7)``. The old
|
|
74
|
+
``int(p) for p in ... if p.isdigit()`` dropped a component like ``"6rc1"``
|
|
75
|
+
entirely and spuriously rejected valid pre-releases.
|
|
76
|
+
"""
|
|
77
|
+
import re as _re
|
|
78
|
+
|
|
79
|
+
out: list[int] = []
|
|
80
|
+
for p in found.split(".")[:2]:
|
|
81
|
+
m = _re.match(r"\d+", p)
|
|
82
|
+
if m:
|
|
83
|
+
out.append(int(m.group()))
|
|
84
|
+
return tuple(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
_found = _dist_version("discopt")
|
|
89
|
+
_v = _version_prefix(_found)
|
|
90
|
+
if _v < _MIN_DISCOPT:
|
|
91
|
+
raise ImportError(
|
|
92
|
+
f"discopt-doe requires discopt>={'.'.join(map(str, _MIN_DISCOPT))} "
|
|
93
|
+
f"(found {_found}); upgrade with `pip install -U discopt`."
|
|
94
|
+
)
|
|
95
|
+
except _PkgNotFound: # odd dev setups (no dist metadata): don't block
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
from discopt.doe.acquisition import (
|
|
99
|
+
ACQUISITIONS,
|
|
100
|
+
confidence_bound,
|
|
101
|
+
expected_improvement,
|
|
102
|
+
resolve_acquisition,
|
|
103
|
+
steepest_ascent,
|
|
104
|
+
)
|
|
105
|
+
from discopt.doe.anova import AnovaEffect, AnovaTable, anova_report
|
|
106
|
+
from discopt.doe.design import (
|
|
107
|
+
BatchDesignResult,
|
|
108
|
+
BatchStrategy,
|
|
109
|
+
DesignConstraint,
|
|
110
|
+
DesignCriterion,
|
|
111
|
+
DesignResult,
|
|
112
|
+
batch_optimal_experiment,
|
|
113
|
+
optimal_experiment,
|
|
114
|
+
project_to_simplex,
|
|
115
|
+
sample_simplex,
|
|
116
|
+
sum_constraint,
|
|
117
|
+
)
|
|
118
|
+
from discopt.doe.discrimination import (
|
|
119
|
+
DiscriminationCriterion,
|
|
120
|
+
DiscriminationDesignResult,
|
|
121
|
+
discriminate_compound,
|
|
122
|
+
discriminate_design,
|
|
123
|
+
evaluate_discrimination_criterion,
|
|
124
|
+
)
|
|
125
|
+
from discopt.doe.discrimination_sequential import (
|
|
126
|
+
DiscriminationRound,
|
|
127
|
+
sequential_discrimination,
|
|
128
|
+
)
|
|
129
|
+
from discopt.doe.estimability import (
|
|
130
|
+
EstimabilityResult,
|
|
131
|
+
collinearity_index,
|
|
132
|
+
d_optimal_subset,
|
|
133
|
+
estimability_rank,
|
|
134
|
+
)
|
|
135
|
+
from discopt.doe.exploration import (
|
|
136
|
+
ExplorationResult,
|
|
137
|
+
explore_design_space,
|
|
138
|
+
)
|
|
139
|
+
from discopt.doe.fim import (
|
|
140
|
+
FIMResult,
|
|
141
|
+
IdentifiabilityDiagnostics,
|
|
142
|
+
IdentifiabilityResult,
|
|
143
|
+
check_identifiability,
|
|
144
|
+
compute_fim,
|
|
145
|
+
diagnose_identifiability,
|
|
146
|
+
)
|
|
147
|
+
from discopt.doe.fractional import (
|
|
148
|
+
fractional_factorial_design,
|
|
149
|
+
)
|
|
150
|
+
from discopt.doe.latin import (
|
|
151
|
+
LatinDesign,
|
|
152
|
+
graeco_latin_square,
|
|
153
|
+
hyper_graeco_latin_square,
|
|
154
|
+
latin_square,
|
|
155
|
+
latin_square_design,
|
|
156
|
+
)
|
|
157
|
+
from discopt.doe.model_based import (
|
|
158
|
+
ModelBasedRoundResult,
|
|
159
|
+
ParametricSurrogate,
|
|
160
|
+
model_based_optimize_round,
|
|
161
|
+
)
|
|
162
|
+
from discopt.doe.optimize import (
|
|
163
|
+
OptimizationCriterion,
|
|
164
|
+
OptimizationRoundResult,
|
|
165
|
+
optimize_round,
|
|
166
|
+
)
|
|
167
|
+
from discopt.doe.profile import (
|
|
168
|
+
ProfileLikelihoodResult,
|
|
169
|
+
profile_all,
|
|
170
|
+
profile_likelihood,
|
|
171
|
+
)
|
|
172
|
+
from discopt.doe.screening import (
|
|
173
|
+
FactorialDesign,
|
|
174
|
+
effects_estimates,
|
|
175
|
+
factorial_2level_design,
|
|
176
|
+
)
|
|
177
|
+
from discopt.doe.selection import (
|
|
178
|
+
ModelSelectionResult,
|
|
179
|
+
likelihood_ratio_test,
|
|
180
|
+
model_selection,
|
|
181
|
+
vuong_test,
|
|
182
|
+
)
|
|
183
|
+
from discopt.doe.sequential import (
|
|
184
|
+
DoERound,
|
|
185
|
+
sequential_doe,
|
|
186
|
+
)
|
|
187
|
+
from discopt.doe.surrogate import (
|
|
188
|
+
PRESETS as SURROGATE_PRESETS,
|
|
189
|
+
)
|
|
190
|
+
from discopt.doe.surrogate import (
|
|
191
|
+
Surrogate,
|
|
192
|
+
coerce_surrogate,
|
|
193
|
+
)
|
|
194
|
+
from discopt.doe.templates import (
|
|
195
|
+
TEMPLATE_NAMES,
|
|
196
|
+
build_template,
|
|
197
|
+
linear_template,
|
|
198
|
+
polynomial_1d_template,
|
|
199
|
+
response_surface_template,
|
|
200
|
+
scheffe_linear_template,
|
|
201
|
+
scheffe_quadratic_template,
|
|
202
|
+
scheffe_special_cubic_template,
|
|
203
|
+
simplex_centroid_points,
|
|
204
|
+
simplex_lattice_points,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
__all__ = [
|
|
208
|
+
"AnovaEffect",
|
|
209
|
+
"AnovaTable",
|
|
210
|
+
"BatchDesignResult",
|
|
211
|
+
"BatchStrategy",
|
|
212
|
+
"DesignConstraint",
|
|
213
|
+
"DesignCriterion",
|
|
214
|
+
"DesignResult",
|
|
215
|
+
"FactorialDesign",
|
|
216
|
+
"LatinDesign",
|
|
217
|
+
"DiscriminationCriterion",
|
|
218
|
+
"DiscriminationDesignResult",
|
|
219
|
+
"DiscriminationRound",
|
|
220
|
+
"DoERound",
|
|
221
|
+
"EstimabilityResult",
|
|
222
|
+
"ExplorationResult",
|
|
223
|
+
"FIMResult",
|
|
224
|
+
"IdentifiabilityDiagnostics",
|
|
225
|
+
"IdentifiabilityResult",
|
|
226
|
+
"ModelBasedRoundResult",
|
|
227
|
+
"ModelSelectionResult",
|
|
228
|
+
"OptimizationCriterion",
|
|
229
|
+
"OptimizationRoundResult",
|
|
230
|
+
"ParametricSurrogate",
|
|
231
|
+
"ProfileLikelihoodResult",
|
|
232
|
+
"SURROGATE_PRESETS",
|
|
233
|
+
"Surrogate",
|
|
234
|
+
"ACQUISITIONS",
|
|
235
|
+
"TEMPLATE_NAMES",
|
|
236
|
+
"anova_report",
|
|
237
|
+
"batch_optimal_experiment",
|
|
238
|
+
"build_template",
|
|
239
|
+
"coerce_surrogate",
|
|
240
|
+
"confidence_bound",
|
|
241
|
+
"expected_improvement",
|
|
242
|
+
"optimize_round",
|
|
243
|
+
"resolve_acquisition",
|
|
244
|
+
"steepest_ascent",
|
|
245
|
+
"check_identifiability",
|
|
246
|
+
"graeco_latin_square",
|
|
247
|
+
"hyper_graeco_latin_square",
|
|
248
|
+
"latin_square",
|
|
249
|
+
"latin_square_design",
|
|
250
|
+
"collinearity_index",
|
|
251
|
+
"compute_fim",
|
|
252
|
+
"d_optimal_subset",
|
|
253
|
+
"diagnose_identifiability",
|
|
254
|
+
"discriminate_compound",
|
|
255
|
+
"effects_estimates",
|
|
256
|
+
"factorial_2level_design",
|
|
257
|
+
"fractional_factorial_design",
|
|
258
|
+
"discriminate_design",
|
|
259
|
+
"estimability_rank",
|
|
260
|
+
"evaluate_discrimination_criterion",
|
|
261
|
+
"explore_design_space",
|
|
262
|
+
"likelihood_ratio_test",
|
|
263
|
+
"linear_template",
|
|
264
|
+
"model_based_optimize_round",
|
|
265
|
+
"model_selection",
|
|
266
|
+
"optimal_experiment",
|
|
267
|
+
"polynomial_1d_template",
|
|
268
|
+
"project_to_simplex",
|
|
269
|
+
"response_surface_template",
|
|
270
|
+
"profile_all",
|
|
271
|
+
"profile_likelihood",
|
|
272
|
+
"sample_simplex",
|
|
273
|
+
"scheffe_linear_template",
|
|
274
|
+
"scheffe_quadratic_template",
|
|
275
|
+
"scheffe_special_cubic_template",
|
|
276
|
+
"sequential_discrimination",
|
|
277
|
+
"sequential_doe",
|
|
278
|
+
"simplex_centroid_points",
|
|
279
|
+
"simplex_lattice_points",
|
|
280
|
+
"sum_constraint",
|
|
281
|
+
"vuong_test",
|
|
282
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Acquisition functions for active-learning optimization.
|
|
2
|
+
|
|
3
|
+
An acquisition function scores candidate design points by how
|
|
4
|
+
*valuable* they are to evaluate next. Higher scores are better; the
|
|
5
|
+
optimizer selects the top-``batch_size`` candidates.
|
|
6
|
+
|
|
7
|
+
Every function in this module has the signature::
|
|
8
|
+
|
|
9
|
+
score(surrogate, X_candidates, *, y_best, direction, **kw) -> ndarray
|
|
10
|
+
|
|
11
|
+
* ``surrogate`` is a fitted object satisfying the
|
|
12
|
+
:class:`~discopt.doe.surrogate.Surrogate` protocol.
|
|
13
|
+
* ``X_candidates`` is a 2D array of shape ``(n, d)``.
|
|
14
|
+
* ``y_best`` is the best observed response so far (incumbent).
|
|
15
|
+
* ``direction`` is ``+1`` for *maximize* or ``-1`` for *minimize*.
|
|
16
|
+
|
|
17
|
+
Implementations
|
|
18
|
+
---------------
|
|
19
|
+
|
|
20
|
+
* :func:`expected_improvement` -- the standard EI used in Bayesian
|
|
21
|
+
optimization. Balances exploitation (high predicted mean in the
|
|
22
|
+
desired direction) and exploration (high uncertainty).
|
|
23
|
+
* :func:`upper_confidence_bound` / :func:`lower_confidence_bound` --
|
|
24
|
+
UCB / LCB. A direction-aware wrapper :func:`confidence_bound` picks
|
|
25
|
+
the right sign automatically.
|
|
26
|
+
* :func:`steepest_ascent` -- Box-Wilson style: score points by the
|
|
27
|
+
predicted improvement only (ignores uncertainty). Useful with a
|
|
28
|
+
response-surface surrogate when you want classical RSM behaviour.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from typing import Callable, Literal
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from discopt.doe.surrogate import Surrogate
|
|
39
|
+
|
|
40
|
+
Direction = Literal[1, -1]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _erf(x: np.ndarray) -> np.ndarray:
|
|
44
|
+
return np.asarray(np.vectorize(math.erf)(x), dtype=float)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _norm_cdf(x: np.ndarray) -> np.ndarray:
|
|
48
|
+
return 0.5 * (1.0 + _erf(x / math.sqrt(2.0)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _norm_pdf(x: np.ndarray) -> np.ndarray:
|
|
52
|
+
return np.asarray(np.exp(-0.5 * x * x) / math.sqrt(2.0 * math.pi), dtype=float)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def expected_improvement(
|
|
56
|
+
surrogate: Surrogate,
|
|
57
|
+
X_candidates: np.ndarray,
|
|
58
|
+
*,
|
|
59
|
+
y_best: float,
|
|
60
|
+
direction: Direction,
|
|
61
|
+
xi: float = 0.0,
|
|
62
|
+
) -> np.ndarray:
|
|
63
|
+
"""Expected improvement over the incumbent ``y_best``.
|
|
64
|
+
|
|
65
|
+
For maximization (``direction = +1``)::
|
|
66
|
+
|
|
67
|
+
EI(x) = (μ - y_best - xi) Φ(z) + σ φ(z)
|
|
68
|
+
z = (μ - y_best - xi) / σ
|
|
69
|
+
|
|
70
|
+
For minimization (``direction = -1``), the sign flips so EI is
|
|
71
|
+
always non-negative for points that improve on the incumbent.
|
|
72
|
+
|
|
73
|
+
``xi`` is an exploration knob -- a small positive value
|
|
74
|
+
(e.g. ``0.01``) demands a slightly stronger improvement and
|
|
75
|
+
encourages exploration.
|
|
76
|
+
"""
|
|
77
|
+
mu, sigma = surrogate.predict(np.asarray(X_candidates, dtype=float))
|
|
78
|
+
mu = np.asarray(mu, dtype=float).ravel()
|
|
79
|
+
sigma = np.asarray(sigma, dtype=float).ravel()
|
|
80
|
+
dir_sign = int(direction)
|
|
81
|
+
if dir_sign not in (1, -1):
|
|
82
|
+
raise ValueError(f"direction must be +1 or -1, got {dir_sign}")
|
|
83
|
+
|
|
84
|
+
improvement = dir_sign * (mu - y_best) - xi
|
|
85
|
+
safe_sigma = np.where(sigma > 0.0, sigma, 1.0)
|
|
86
|
+
z = improvement / safe_sigma
|
|
87
|
+
ei = improvement * _norm_cdf(z) + safe_sigma * _norm_pdf(z)
|
|
88
|
+
ei = np.where(sigma > 0.0, ei, np.maximum(improvement, 0.0))
|
|
89
|
+
return ei
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def upper_confidence_bound(
|
|
93
|
+
surrogate: Surrogate,
|
|
94
|
+
X_candidates: np.ndarray,
|
|
95
|
+
*,
|
|
96
|
+
kappa: float = 2.0,
|
|
97
|
+
) -> np.ndarray:
|
|
98
|
+
"""``μ + κ σ`` -- maximize when ``direction = +1``."""
|
|
99
|
+
mu, sigma = surrogate.predict(np.asarray(X_candidates, dtype=float))
|
|
100
|
+
return np.asarray(mu).ravel() + float(kappa) * np.asarray(sigma).ravel()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def lower_confidence_bound(
|
|
104
|
+
surrogate: Surrogate,
|
|
105
|
+
X_candidates: np.ndarray,
|
|
106
|
+
*,
|
|
107
|
+
kappa: float = 2.0,
|
|
108
|
+
) -> np.ndarray:
|
|
109
|
+
"""``-(μ - κ σ)`` -- larger is *better* under minimization."""
|
|
110
|
+
mu, sigma = surrogate.predict(np.asarray(X_candidates, dtype=float))
|
|
111
|
+
return -(np.asarray(mu).ravel() - float(kappa) * np.asarray(sigma).ravel())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def confidence_bound(
|
|
115
|
+
surrogate: Surrogate,
|
|
116
|
+
X_candidates: np.ndarray,
|
|
117
|
+
*,
|
|
118
|
+
direction: Direction,
|
|
119
|
+
kappa: float = 2.0,
|
|
120
|
+
) -> np.ndarray:
|
|
121
|
+
"""Direction-aware UCB/LCB. Higher score = better candidate."""
|
|
122
|
+
if direction == 1:
|
|
123
|
+
return upper_confidence_bound(surrogate, X_candidates, kappa=kappa)
|
|
124
|
+
if direction == -1:
|
|
125
|
+
return lower_confidence_bound(surrogate, X_candidates, kappa=kappa)
|
|
126
|
+
raise ValueError(f"direction must be +1 or -1, got {direction}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def steepest_ascent(
|
|
130
|
+
surrogate: Surrogate,
|
|
131
|
+
X_candidates: np.ndarray,
|
|
132
|
+
*,
|
|
133
|
+
direction: Direction,
|
|
134
|
+
y_best: float | None = None,
|
|
135
|
+
) -> np.ndarray:
|
|
136
|
+
"""Predicted improvement only -- ignores uncertainty.
|
|
137
|
+
|
|
138
|
+
Score is ``direction * μ(x)`` (or, if ``y_best`` is supplied,
|
|
139
|
+
``direction * (μ(x) - y_best)``, which only shifts the score and
|
|
140
|
+
does not change the ranking). Use this with a response-surface
|
|
141
|
+
surrogate to reproduce classical Box-Wilson behaviour.
|
|
142
|
+
"""
|
|
143
|
+
mu, _sigma = surrogate.predict(np.asarray(X_candidates, dtype=float))
|
|
144
|
+
mu = np.asarray(mu, dtype=float).ravel()
|
|
145
|
+
if y_best is None:
|
|
146
|
+
return int(direction) * mu
|
|
147
|
+
return int(direction) * (mu - float(y_best))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Note: "ucb" and "lcb" both resolve to the *direction-aware* confidence_bound
|
|
151
|
+
# wrapper, which picks upper (maximize) or lower (minimize) from the round's
|
|
152
|
+
# direction. So under maximization "lcb" still computes the UCB (and vice
|
|
153
|
+
# versa) -- the names are aliases for "confidence bound", not a hard choice of
|
|
154
|
+
# upper vs lower. Use the raw upper_confidence_bound / lower_confidence_bound
|
|
155
|
+
# callables directly if you need a fixed side.
|
|
156
|
+
ACQUISITIONS: dict[str, Callable[..., np.ndarray]] = {
|
|
157
|
+
"expected_improvement": expected_improvement,
|
|
158
|
+
"ei": expected_improvement,
|
|
159
|
+
"ucb": confidence_bound,
|
|
160
|
+
"lcb": confidence_bound,
|
|
161
|
+
"confidence_bound": confidence_bound,
|
|
162
|
+
"steepest_ascent": steepest_ascent,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def resolve_acquisition(name_or_fn):
|
|
167
|
+
"""Look up an acquisition function by string or pass through a callable."""
|
|
168
|
+
if callable(name_or_fn):
|
|
169
|
+
return name_or_fn
|
|
170
|
+
try:
|
|
171
|
+
return ACQUISITIONS[name_or_fn]
|
|
172
|
+
except KeyError as e:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"unknown acquisition {name_or_fn!r}; available: {sorted(set(ACQUISITIONS))}"
|
|
175
|
+
) from e
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def call_acquisition(acq_fn, surrogate, X_candidates, *, direction, y_best, acq_kwargs=None):
|
|
179
|
+
"""Invoke an acquisition, passing only the arguments it accepts.
|
|
180
|
+
|
|
181
|
+
Replaces a ``try/except TypeError`` dispatch that both masked genuine
|
|
182
|
+
errors raised *inside* an acquisition and silently discarded the caller's
|
|
183
|
+
tuning kwargs. ``direction``/``y_best`` are injected only when the callable
|
|
184
|
+
declares them (or has ``**kwargs``), and user ``acq_kwargs`` that the
|
|
185
|
+
callable cannot consume raise a clear error instead of being swallowed.
|
|
186
|
+
"""
|
|
187
|
+
import inspect
|
|
188
|
+
|
|
189
|
+
acq_kwargs = dict(acq_kwargs or {})
|
|
190
|
+
params = inspect.signature(acq_fn).parameters
|
|
191
|
+
has_var_kw = any(p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values())
|
|
192
|
+
named = {
|
|
193
|
+
n
|
|
194
|
+
for n, p in params.items()
|
|
195
|
+
if p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
196
|
+
}
|
|
197
|
+
if not has_var_kw:
|
|
198
|
+
bad = [k for k in acq_kwargs if k not in named]
|
|
199
|
+
if bad:
|
|
200
|
+
tunable = sorted(named - {"surrogate", "X_candidates", "direction", "y_best"})
|
|
201
|
+
raise TypeError(
|
|
202
|
+
f"acquisition {getattr(acq_fn, '__name__', acq_fn)!r} got unexpected "
|
|
203
|
+
f"keyword(s) {bad}; it accepts {tunable}."
|
|
204
|
+
)
|
|
205
|
+
call_kwargs = dict(acq_kwargs)
|
|
206
|
+
if "direction" in named or has_var_kw:
|
|
207
|
+
call_kwargs["direction"] = direction
|
|
208
|
+
if "y_best" in named or has_var_kw:
|
|
209
|
+
call_kwargs["y_best"] = y_best
|
|
210
|
+
return acq_fn(surrogate, X_candidates, **call_kwargs)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"ACQUISITIONS",
|
|
215
|
+
"confidence_bound",
|
|
216
|
+
"expected_improvement",
|
|
217
|
+
"lower_confidence_bound",
|
|
218
|
+
"resolve_acquisition",
|
|
219
|
+
"steepest_ascent",
|
|
220
|
+
"upper_confidence_bound",
|
|
221
|
+
]
|