spotoptim 1.0.2__tar.gz → 2.0.0__tar.gz
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.
- {spotoptim-1.0.2 → spotoptim-2.0.0}/PKG-INFO +1 -1
- {spotoptim-1.0.2 → spotoptim-2.0.0}/pyproject.toml +2 -2
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/SpotOptim.py +122 -20
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/__init__.py +4 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/so.py +102 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/kernels.py +5 -3
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/kriging.py +4 -2
- {spotoptim-1.0.2 → spotoptim-2.0.0}/README.md +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/core/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/core/data.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/core/experiment.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/core/protocol.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/core/storage.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/data/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/data/base.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/data/diabetes.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/datasets/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/datasets/py.typed +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/datasets/test01.csv +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/datasets/test02.csv +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/datasets/test11.csv +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/eda/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/eda/plots.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/cd_data.csv +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/forr08a.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/mo.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/remote.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/function/torch_objective.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/hyperparameters/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/hyperparameters/parameters.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/inspection/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/inspection/importance.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/inspection/predictions.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/mo/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/mo/mo_mm.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/mo/pareto.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/nn/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/nn/linear_regressor.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/nn/mlp.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/optimizer/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/optimizer/acquisition.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/optimizer/schedule_free.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/optimizer/wrapper.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/plot/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/plot/contour.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/plot/mo.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/plot/visualization.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/py.typed +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/reporting/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/reporting/analysis.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/reporting/results.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/sampling/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/sampling/design.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/sampling/effects.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/sampling/lhs.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/sampling/mm.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/nystroem.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/pipeline.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/surrogate/simple_kriging.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/tricands/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/tricands/tricands.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/__init__.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/boundaries.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/convert.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/dimreduction.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/eval.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/file.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/mapping.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/ocba.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/pca.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/scaler.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/serialization.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/stats.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/tensorboard.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/transform.py +0 -0
- {spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/utils/variables.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spotoptim"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "Sequential Parameter Optimization Toolbox"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "AGPL-3.0-or-later" }
|
|
@@ -104,7 +104,7 @@ filterwarnings = [
|
|
|
104
104
|
]
|
|
105
105
|
|
|
106
106
|
[build-system]
|
|
107
|
-
requires = ["uv_build>=0.9.18
|
|
107
|
+
requires = ["uv_build>=0.9.18"]
|
|
108
108
|
build-backend = "uv_build"
|
|
109
109
|
|
|
110
110
|
[tool.uv]
|
|
@@ -29,6 +29,7 @@ from spotoptim.utils import dimreduction as _dimred
|
|
|
29
29
|
from spotoptim.optimizer import acquisition as _acq
|
|
30
30
|
from spotoptim.core import storage as _storage
|
|
31
31
|
from spotoptim.optimizer.wrapper import gpr_minimize_wrapper
|
|
32
|
+
from spotoptim.surrogate import Kriging
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
@dataclass
|
|
@@ -58,7 +59,14 @@ class SpotOptimConfig:
|
|
|
58
59
|
verbose (bool): Whether to print verbose output.
|
|
59
60
|
warnings_filter (Literal["default", "error", "ignore"]): Filter for warnings.
|
|
60
61
|
n_infill_points (int): Number of infill points.
|
|
61
|
-
max_surrogate_points (Optional[Union[int, List[int]]]): Maximum number of
|
|
62
|
+
max_surrogate_points (Optional[Union[int, List[int]]]): Maximum number of
|
|
63
|
+
points used to fit the surrogate. Defaults to ``300`` -- large enough
|
|
64
|
+
that typical Bayesian-optimization budgets (a few hundred evaluations)
|
|
65
|
+
are fit on all points (full surrogate quality), while bounding the
|
|
66
|
+
O(n^3) GP-fit cost on very long runs. Once the evaluation count exceeds
|
|
67
|
+
the cap the training set is subsampled via ``selection_method``. Pass
|
|
68
|
+
``None`` to always fit on all points, or a smaller int to bound cost
|
|
69
|
+
further.
|
|
62
70
|
selection_method (str): Method for selecting infill points.
|
|
63
71
|
acquisition_failure_strategy (str): Strategy for handling acquisition function failures.
|
|
64
72
|
penalty (bool): Whether to use penalty.
|
|
@@ -81,6 +89,10 @@ class SpotOptimConfig:
|
|
|
81
89
|
acquisition_optimizer_kwargs (Optional[Dict[str, Any]]): Keyword arguments for the acquisition function optimizer.
|
|
82
90
|
args (Tuple): Arguments for the objective function.
|
|
83
91
|
kwargs (Optional[Dict[str, Any]]): Keyword arguments for the objective function.
|
|
92
|
+
metric_factorial (str): Distance metric used by a factor-aware Kriging surrogate for
|
|
93
|
+
nominal (categorical) variables. ``"hamming"`` (default) treats all distinct
|
|
94
|
+
factor levels as equidistant, which is correct for unordered factors.
|
|
95
|
+
Any scipy pairwise distance metric string is accepted.
|
|
84
96
|
|
|
85
97
|
|
|
86
98
|
Examples:
|
|
@@ -116,7 +128,7 @@ class SpotOptimConfig:
|
|
|
116
128
|
verbose=False,
|
|
117
129
|
warnings_filter="ignore",
|
|
118
130
|
n_infill_points=1,
|
|
119
|
-
max_surrogate_points=
|
|
131
|
+
max_surrogate_points=300,
|
|
120
132
|
selection_method="distant",
|
|
121
133
|
acquisition_failure_strategy="random",
|
|
122
134
|
penalty=False,
|
|
@@ -161,7 +173,7 @@ class SpotOptimConfig:
|
|
|
161
173
|
verbose: bool = False
|
|
162
174
|
warnings_filter: Literal["default", "error", "ignore"] = "ignore"
|
|
163
175
|
n_infill_points: int = 1
|
|
164
|
-
max_surrogate_points: Optional[Union[int, List[int]]] =
|
|
176
|
+
max_surrogate_points: Optional[Union[int, List[int]]] = 300
|
|
165
177
|
selection_method: str = "distant"
|
|
166
178
|
acquisition_failure_strategy: str = "random"
|
|
167
179
|
penalty: bool = False
|
|
@@ -181,6 +193,7 @@ class SpotOptimConfig:
|
|
|
181
193
|
acquisition_optimizer_kwargs: Optional[Dict[str, Any]] = None
|
|
182
194
|
args: Tuple = ()
|
|
183
195
|
kwargs: Optional[Dict[str, Any]] = None
|
|
196
|
+
metric_factorial: str = "hamming"
|
|
184
197
|
|
|
185
198
|
def __post_init__(self):
|
|
186
199
|
if self.kwargs is None:
|
|
@@ -351,7 +364,8 @@ class SpotOptim(BaseEstimator):
|
|
|
351
364
|
max_surrogate_points (int, optional):
|
|
352
365
|
Maximum number of points to use for surrogate model fitting.
|
|
353
366
|
If None, all points are used. If the number of evaluated points exceeds this limit,
|
|
354
|
-
a subset is selected using the selection method. Defaults to
|
|
367
|
+
a subset is selected using the selection method. Defaults to 300 (covers
|
|
368
|
+
typical budgets at full quality while bounding GP-fit cost; pass None for all points).
|
|
355
369
|
selection_method (str, optional):
|
|
356
370
|
Method for selecting points when max_surrogate_points is exceeded.
|
|
357
371
|
Options: 'distant' (Select points that are distant from each other via K-means clustering) or
|
|
@@ -428,6 +442,13 @@ class SpotOptim(BaseEstimator):
|
|
|
428
442
|
* "cosine": Cosine distance.
|
|
429
443
|
* "correlation": Correlation distance.
|
|
430
444
|
* "canberra", "braycurtis", "sqeuclidean", etc.
|
|
445
|
+
metric_factorial (str, optional):
|
|
446
|
+
Distance metric used for nominal (factor) variables by a factor-aware
|
|
447
|
+
Kriging surrogate. ``"hamming"`` (default) treats all distinct factor
|
|
448
|
+
levels as equidistant, which is correct for unordered factors; any
|
|
449
|
+
scipy pairwise distance metric string is accepted. Only takes effect
|
|
450
|
+
when factor variables are present and ``surrogate`` is left as ``None``
|
|
451
|
+
(an order-agnostic Kriging is then built automatically).
|
|
431
452
|
|
|
432
453
|
Attributes:
|
|
433
454
|
X_ (ndarray): All evaluated points, shape (n_samples, n_features).
|
|
@@ -733,7 +754,7 @@ class SpotOptim(BaseEstimator):
|
|
|
733
754
|
verbose: bool = False,
|
|
734
755
|
warnings_filter: Literal["default", "error", "ignore"] = "ignore",
|
|
735
756
|
n_infill_points: int = 1,
|
|
736
|
-
max_surrogate_points: Optional[Union[int, List[int]]] =
|
|
757
|
+
max_surrogate_points: Optional[Union[int, List[int]]] = 300,
|
|
737
758
|
selection_method: str = "distant",
|
|
738
759
|
acquisition_failure_strategy: str = "random",
|
|
739
760
|
penalty: bool = False,
|
|
@@ -753,6 +774,7 @@ class SpotOptim(BaseEstimator):
|
|
|
753
774
|
acquisition_optimizer_kwargs: Optional[Dict[str, Any]] = None,
|
|
754
775
|
args: Tuple = (),
|
|
755
776
|
kwargs: Optional[Dict[str, Any]] = None,
|
|
777
|
+
metric_factorial: str = "hamming",
|
|
756
778
|
):
|
|
757
779
|
warnings.filterwarnings(warnings_filter)
|
|
758
780
|
|
|
@@ -831,6 +853,7 @@ class SpotOptim(BaseEstimator):
|
|
|
831
853
|
acquisition_optimizer_kwargs=acquisition_optimizer_kwargs,
|
|
832
854
|
args=args,
|
|
833
855
|
kwargs=kwargs,
|
|
856
|
+
metric_factorial=metric_factorial,
|
|
834
857
|
)
|
|
835
858
|
|
|
836
859
|
# Initialize State
|
|
@@ -900,6 +923,8 @@ class SpotOptim(BaseEstimator):
|
|
|
900
923
|
|
|
901
924
|
# Initialize surrogate model(s)
|
|
902
925
|
self.init_surrogate()
|
|
926
|
+
# Warn if factor variables are being modelled with a factor-blind surrogate
|
|
927
|
+
self._validate_factor_surrogate_compat()
|
|
903
928
|
|
|
904
929
|
# Design generator (from scipy.stats.qmc)
|
|
905
930
|
self.lhs_sampler = LatinHypercube(d=self.n_dim, rng=self.seed)
|
|
@@ -1637,24 +1662,89 @@ class SpotOptim(BaseEstimator):
|
|
|
1637
1662
|
self._max_surrogate_points_list = None
|
|
1638
1663
|
self._active_max_surrogate_points = self.config.max_surrogate_points
|
|
1639
1664
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1665
|
+
if self._factor_maps:
|
|
1666
|
+
# Factor variables are present: use a nominal (order-agnostic) Kriging
|
|
1667
|
+
# surrogate so the kernel treats factor levels as equidistant.
|
|
1668
|
+
# var_type is already the reduced var_type (after setup_dimension_reduction).
|
|
1669
|
+
self.surrogate = Kriging(
|
|
1670
|
+
var_type=list(self.var_type),
|
|
1671
|
+
metric_factorial=self.config.metric_factorial,
|
|
1672
|
+
seed=self.seed if self.seed is not None else 124,
|
|
1673
|
+
)
|
|
1674
|
+
else:
|
|
1675
|
+
# No factor variables: use the standard GaussianProcessRegressor.
|
|
1676
|
+
kernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(
|
|
1677
|
+
length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5
|
|
1678
|
+
)
|
|
1643
1679
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1680
|
+
# Determine optimizer for GPR
|
|
1681
|
+
optimizer = "fmin_l_bfgs_b" # Default used by sklearn
|
|
1682
|
+
if self.config.acquisition_optimizer_kwargs is not None:
|
|
1683
|
+
optimizer = partial(
|
|
1684
|
+
gpr_minimize_wrapper, **self.config.acquisition_optimizer_kwargs
|
|
1685
|
+
)
|
|
1686
|
+
|
|
1687
|
+
self.surrogate = GaussianProcessRegressor(
|
|
1688
|
+
kernel=kernel,
|
|
1689
|
+
n_restarts_optimizer=100,
|
|
1690
|
+
normalize_y=True,
|
|
1691
|
+
random_state=self.seed,
|
|
1692
|
+
optimizer=optimizer,
|
|
1649
1693
|
)
|
|
1650
1694
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1695
|
+
def _validate_factor_surrogate_compat(self) -> None:
|
|
1696
|
+
"""Warn when factor variables are present but the surrogate is factor-blind.
|
|
1697
|
+
|
|
1698
|
+
Called once after ``init_surrogate`` — at that point ``self._factor_maps``
|
|
1699
|
+
is fully populated and the surrogate is set. Fires only when there is at
|
|
1700
|
+
least one factor dimension. Issues a ``UserWarning`` (not an exception) so
|
|
1701
|
+
that users can still proceed but are informed of the mismatch.
|
|
1702
|
+
|
|
1703
|
+
Warns in two situations:
|
|
1704
|
+
|
|
1705
|
+
* The surrogate is not a :class:`~spotoptim.surrogate.Kriging` at all
|
|
1706
|
+
(e.g. sklearn GPR, ``SimpleKriging``, ``RandomForestRegressor``). Such
|
|
1707
|
+
surrogates treat factor integer codes as ordinal numbers, which is
|
|
1708
|
+
incorrect for unordered categorical variables.
|
|
1709
|
+
|
|
1710
|
+
* The surrogate IS a :class:`~spotoptim.surrogate.Kriging` but its
|
|
1711
|
+
``metric_factorial`` is ``"canberra"``, which is order-dependent and
|
|
1712
|
+
singles out level index 0.
|
|
1713
|
+
|
|
1714
|
+
Returns:
|
|
1715
|
+
None
|
|
1716
|
+
"""
|
|
1717
|
+
if not self._factor_maps:
|
|
1718
|
+
return # No factor variables → nothing to validate
|
|
1719
|
+
|
|
1720
|
+
# Use a nested catch_warnings context to ensure this one-time configuration
|
|
1721
|
+
# warning is always visible to the caller, even when the SpotOptim instance
|
|
1722
|
+
# was created with warnings_filter="ignore" (the default). Users who want to
|
|
1723
|
+
# silence it permanently can use warnings.filterwarnings("ignore", ...) before
|
|
1724
|
+
# calling SpotOptim.
|
|
1725
|
+
with warnings.catch_warnings():
|
|
1726
|
+
warnings.simplefilter("always")
|
|
1727
|
+
if not isinstance(self.surrogate, Kriging):
|
|
1728
|
+
warnings.warn(
|
|
1729
|
+
f"Factor variables detected (original bounds dimensions: "
|
|
1730
|
+
f"{sorted(self._factor_maps.keys())}) "
|
|
1731
|
+
f"but the active surrogate ({type(self.surrogate).__name__}) does not "
|
|
1732
|
+
f"support nominal (order-agnostic) factor metrics. Factor integer codes "
|
|
1733
|
+
f"will be treated as ordinal numbers, which may mislead the surrogate. "
|
|
1734
|
+
f"Consider using a factor-aware Kriging surrogate: "
|
|
1735
|
+
f"Kriging(metric_factorial='hamming').",
|
|
1736
|
+
UserWarning,
|
|
1737
|
+
stacklevel=3,
|
|
1738
|
+
)
|
|
1739
|
+
elif self.surrogate.metric_factorial == "canberra":
|
|
1740
|
+
warnings.warn(
|
|
1741
|
+
"Factor variables detected but the Kriging surrogate uses "
|
|
1742
|
+
"metric_factorial='canberra', which is order-dependent and singles "
|
|
1743
|
+
"out factor level index 0. Use metric_factorial='hamming' for "
|
|
1744
|
+
"unordered (nominal) factor variables.",
|
|
1745
|
+
UserWarning,
|
|
1746
|
+
stacklevel=3,
|
|
1747
|
+
)
|
|
1658
1748
|
|
|
1659
1749
|
def get_initial_design(self, X0: Optional[np.ndarray] = None) -> np.ndarray:
|
|
1660
1750
|
"""Generate or process initial design points. Ensures that design points are in
|
|
@@ -2046,6 +2136,18 @@ class SpotOptim(BaseEstimator):
|
|
|
2046
2136
|
)
|
|
2047
2137
|
X_fit, y_fit = self.fit_selection_dispatcher(X, y)
|
|
2048
2138
|
|
|
2139
|
+
# B3: Propagate the reduced var_type to the surrogate before fitting.
|
|
2140
|
+
# This activates the factor kernel mask on a user-provided Kriging whose
|
|
2141
|
+
# var_type is still at its constructor default (None → ["float"]).
|
|
2142
|
+
# self.var_type is already the reduced var_type here (dimension reduction
|
|
2143
|
+
# runs in setup_dimension_reduction before fitting, and the fit selection
|
|
2144
|
+
# only drops rows). The length guard is a defensive check against an
|
|
2145
|
+
# externally-supplied X whose column count does not match the active space.
|
|
2146
|
+
if hasattr(self.surrogate, "var_type") and len(self.var_type) == X_fit.shape[1]:
|
|
2147
|
+
surrogate_vt = getattr(self.surrogate, "var_type", None)
|
|
2148
|
+
if surrogate_vt is None or surrogate_vt == ["float"]:
|
|
2149
|
+
self.surrogate.var_type = list(self.var_type)
|
|
2150
|
+
|
|
2049
2151
|
self.surrogate.fit(X_fit, y_fit)
|
|
2050
2152
|
|
|
2051
2153
|
def fit_scheduler(self) -> None:
|
|
@@ -16,6 +16,8 @@ from .so import (
|
|
|
16
16
|
lennard_jones,
|
|
17
17
|
robot_arm_obstacle,
|
|
18
18
|
wingwt,
|
|
19
|
+
factor_quadratic,
|
|
20
|
+
FACTOR_QUADRATIC_LEVELS,
|
|
19
21
|
)
|
|
20
22
|
from .mo import (
|
|
21
23
|
activity_pred,
|
|
@@ -46,6 +48,8 @@ __all__ = [
|
|
|
46
48
|
"lennard_jones",
|
|
47
49
|
"robot_arm_obstacle",
|
|
48
50
|
"wingwt",
|
|
51
|
+
"factor_quadratic",
|
|
52
|
+
"FACTOR_QUADRATIC_LEVELS",
|
|
49
53
|
"mo_conv2_min",
|
|
50
54
|
"fonseca_fleming",
|
|
51
55
|
"kursawe",
|
|
@@ -8,6 +8,8 @@ Analytical single-objective test functions for optimization benchmarking.
|
|
|
8
8
|
This module provides well-known analytical test functions commonly used for evaluating and benchmarking optimization algorithms.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
11
13
|
import numpy as np
|
|
12
14
|
|
|
13
15
|
|
|
@@ -623,6 +625,106 @@ def robot_arm_obstacle(X: np.ndarray) -> np.ndarray:
|
|
|
623
625
|
return dist_sq + penalty
|
|
624
626
|
|
|
625
627
|
|
|
628
|
+
#: Default factor levels for :func:`factor_quadratic` — 16 unordered nominal levels.
|
|
629
|
+
FACTOR_QUADRATIC_LEVELS = tuple(f"c{i:02d}" for i in range(16))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@lru_cache(maxsize=None)
|
|
633
|
+
def _factor_quadratic_offsets(n_levels: int) -> np.ndarray:
|
|
634
|
+
"""Fixed, index-scrambled per-level offsets in ``[0, 1]``.
|
|
635
|
+
|
|
636
|
+
Built from a deterministic permutation (fixed seed) so that adjacency in the
|
|
637
|
+
integer level index carries *no* information about the objective value — the
|
|
638
|
+
defining property that makes an ordinal/continuous encoding of the factor
|
|
639
|
+
misleading. Deterministic across calls and processes; the result is cached
|
|
640
|
+
and returned read-only (callers must not mutate it).
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
n_levels (int): Number of factor levels.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
np.ndarray: Offsets of shape ``(n_levels,)``; exactly one entry is ``0.0``
|
|
647
|
+
(the optimal level) and one is ``1.0`` (the worst).
|
|
648
|
+
"""
|
|
649
|
+
perm = np.random.default_rng(0).permutation(n_levels)
|
|
650
|
+
offsets = perm.astype(float) / (n_levels - 1)
|
|
651
|
+
offsets.setflags(write=False) # cached array — prevent accidental mutation
|
|
652
|
+
return offsets
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def factor_quadratic(X, levels=FACTOR_QUADRATIC_LEVELS, amp: float = 8.0) -> np.ndarray:
|
|
656
|
+
"""Mixed continuous + nominal-factor benchmark with index-scrambled level structure.
|
|
657
|
+
|
|
658
|
+
A deliberately adversarial test function with 1 continuous dimension and 1
|
|
659
|
+
*nominal* (unordered) factor with many levels (16 by default). The continuous
|
|
660
|
+
part is a simple bowl shared by all levels; each level adds a vertical offset
|
|
661
|
+
drawn from a fixed index-scrambled permutation, so that **adjacency in the
|
|
662
|
+
integer level index carries no information about the objective value**.
|
|
663
|
+
|
|
664
|
+
A surrogate that treats the factor as an ordinal/continuous integer (Euclidean
|
|
665
|
+
or Canberra distance on the codes) wrongly assumes neighbouring indices behave
|
|
666
|
+
similarly and is systematically misled — most strongly in the data-sparse
|
|
667
|
+
regime typical of expensive black-box optimization, where most levels are
|
|
668
|
+
observed only a few times. A nominal (Hamming) kernel treats all distinct
|
|
669
|
+
levels as equidistant and learns each level's contribution independently, so it
|
|
670
|
+
builds a more accurate surrogate from the same budget.
|
|
671
|
+
|
|
672
|
+
Mathematical definition (with ``n = len(levels)``)::
|
|
673
|
+
|
|
674
|
+
offset = perm / (n - 1) # perm = fixed permutation of {0, .., n-1}
|
|
675
|
+
f(x, c) = x**2 + amp * offset[idx(c)]
|
|
676
|
+
|
|
677
|
+
where ``idx`` maps a factor label to its integer index.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
X (array-like): Input array of shape ``(n_samples, 2)`` or ``(2,)``.
|
|
681
|
+
Column 0 is the continuous variable ``x ∈ [-3, 3]``.
|
|
682
|
+
Column 1 is the factor column, accepted as either string labels (the
|
|
683
|
+
entries of ``levels``) or numeric indices ``0 .. n-1`` — enabling both
|
|
684
|
+
standalone unit testing and direct use by SpotOptim (which passes the
|
|
685
|
+
factor column as string labels).
|
|
686
|
+
levels (tuple of str, optional): Ordered sequence of factor labels. Its
|
|
687
|
+
length sets the number of levels. Defaults to
|
|
688
|
+
:data:`FACTOR_QUADRATIC_LEVELS` (16 levels ``"c00" .. "c15"``).
|
|
689
|
+
amp (float, optional): Scale of the per-level offset term. Defaults to ``8.0``.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
np.ndarray: Function values of shape ``(n_samples,)`` with dtype ``float``.
|
|
693
|
+
|
|
694
|
+
Note:
|
|
695
|
+
**Global optimum** (default 16 levels): ``f(x=0, c="c04") = 0.0``. The
|
|
696
|
+
optimal level is the index where the scrambled offset is ``0``.
|
|
697
|
+
|
|
698
|
+
**Why ordinal encoding fails**: with the default levels the optimum
|
|
699
|
+
(index 4) is flanked in index space by a *bad* level (index 3, offset
|
|
700
|
+
``≈0.67``). An ordinal kernel interpolates between adjacent indices and
|
|
701
|
+
therefore under-ranks the optimal level; a Hamming kernel does not. The
|
|
702
|
+
benefit of the nominal kernel is largest when the number of levels is large
|
|
703
|
+
relative to the evaluation budget — see ``scripts/factor_kernel_benchmark.py``.
|
|
704
|
+
|
|
705
|
+
Examples:
|
|
706
|
+
```{python}
|
|
707
|
+
import numpy as np
|
|
708
|
+
from spotoptim.function import factor_quadratic
|
|
709
|
+
# global optimum: x=0 at level "c04"
|
|
710
|
+
print(factor_quadratic(np.array([[0.0, "c04"]], dtype=object)))
|
|
711
|
+
# a different level is worse
|
|
712
|
+
print(factor_quadratic(np.array([[0.0, "c15"]], dtype=object)))
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
"""
|
|
716
|
+
X = np.atleast_2d(X)
|
|
717
|
+
x = X[:, 0].astype(float) # continuous column
|
|
718
|
+
col = X[:, 1] # factor column: string labels or numeric indices
|
|
719
|
+
lut = {lab: i for i, lab in enumerate(levels)}
|
|
720
|
+
idx = np.array(
|
|
721
|
+
[lut[v] if isinstance(v, str) else int(round(float(v))) for v in col],
|
|
722
|
+
dtype=int,
|
|
723
|
+
)
|
|
724
|
+
offset = _factor_quadratic_offsets(len(levels))
|
|
725
|
+
return x**2 + amp * offset[idx]
|
|
726
|
+
|
|
727
|
+
|
|
626
728
|
def robot_arm_hard(X: np.ndarray) -> np.ndarray:
|
|
627
729
|
"""10-Link Robot Arm with Maze-Like Hard Constraints.
|
|
628
730
|
|
|
@@ -68,7 +68,7 @@ class SpotOptimKernel(Kernel):
|
|
|
68
68
|
|
|
69
69
|
where:
|
|
70
70
|
D_ordered = sum_j theta_j * |x_ij - y_lj|^p (for ordered variables)
|
|
71
|
-
D_factor = sum_j theta_j * d(x_ij, y_lj) (for factor variables, d is metric like
|
|
71
|
+
D_factor = sum_j theta_j * d(x_ij, y_lj) (for factor variables, d is metric like Hamming)
|
|
72
72
|
|
|
73
73
|
Args:
|
|
74
74
|
theta (np.ndarray): The correlation parameters (weights).
|
|
@@ -77,7 +77,9 @@ class SpotOptimKernel(Kernel):
|
|
|
77
77
|
var_type (list of str): List of variable types, e.g. ['float', 'int', 'factor'].
|
|
78
78
|
p_val (float, optional): Power parameter for ordered distance. Defaults to 2.0.
|
|
79
79
|
metric_factorial (str, optional): Metric for factor distance (passed to cdist/pdist).
|
|
80
|
-
Defaults to '
|
|
80
|
+
Defaults to 'hamming'. Hamming is a true nominal (order-agnostic) metric;
|
|
81
|
+
canberra distance on integer level indices is order-dependent and singles out
|
|
82
|
+
index 0. Any scipy distance metric remains selectable via this kwarg.
|
|
81
83
|
"""
|
|
82
84
|
|
|
83
85
|
def __init__(
|
|
@@ -85,7 +87,7 @@ class SpotOptimKernel(Kernel):
|
|
|
85
87
|
theta,
|
|
86
88
|
var_type,
|
|
87
89
|
p_val=2.0,
|
|
88
|
-
metric_factorial="
|
|
90
|
+
metric_factorial="hamming",
|
|
89
91
|
):
|
|
90
92
|
self.theta = np.asanyarray(theta)
|
|
91
93
|
self.var_type = var_type
|
|
@@ -78,7 +78,9 @@ class Kriging(BaseEstimator, RegressorMixin):
|
|
|
78
78
|
min_Lambda (float, optional): Minimum log10(Lambda) bound. Defaults to -9.0.
|
|
79
79
|
max_Lambda (float, optional): Maximum log10(Lambda) bound. Defaults to 0.0.
|
|
80
80
|
metric_factorial (str, optional): Distance metric for factor variables.
|
|
81
|
-
Defaults to "
|
|
81
|
+
Defaults to ``"hamming"``. Hamming is a true nominal (order-agnostic) metric;
|
|
82
|
+
canberra distance on integer level indices is order-dependent and singles out
|
|
83
|
+
index 0. Any scipy distance metric remains selectable via this kwarg.
|
|
82
84
|
isotropic (bool, optional): Use single theta for all dimensions. Defaults to False.
|
|
83
85
|
theta (np.ndarray, optional): Initial theta values (log10 scale). Defaults to None.
|
|
84
86
|
|
|
@@ -163,7 +165,7 @@ class Kriging(BaseEstimator, RegressorMixin):
|
|
|
163
165
|
optim_p: bool = False,
|
|
164
166
|
min_Lambda: float = -9.0,
|
|
165
167
|
max_Lambda: float = 0.0,
|
|
166
|
-
metric_factorial: str = "
|
|
168
|
+
metric_factorial: str = "hamming",
|
|
167
169
|
isotropic: bool = False,
|
|
168
170
|
theta: Optional[np.ndarray] = None,
|
|
169
171
|
**kwargs,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|
{spotoptim-1.0.2 → spotoptim-2.0.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|