spotoptim 2.1.0__tar.gz → 2.1.2__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.
Files changed (85) hide show
  1. {spotoptim-2.1.0 → spotoptim-2.1.2}/PKG-INFO +6 -6
  2. {spotoptim-2.1.0 → spotoptim-2.1.2}/pyproject.toml +6 -6
  3. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/optimizer/acquisition.py +112 -5
  4. {spotoptim-2.1.0 → spotoptim-2.1.2}/README.md +0 -0
  5. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/SpotOptim.py +0 -0
  6. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/__init__.py +0 -0
  7. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/core/__init__.py +0 -0
  8. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/core/data.py +0 -0
  9. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/core/experiment.py +0 -0
  10. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/core/protocol.py +0 -0
  11. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/core/storage.py +0 -0
  12. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/data/__init__.py +0 -0
  13. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/data/base.py +0 -0
  14. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/data/diabetes.py +0 -0
  15. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/datasets/__init__.py +0 -0
  16. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/datasets/py.typed +0 -0
  17. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/datasets/test01.csv +0 -0
  18. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/datasets/test02.csv +0 -0
  19. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/datasets/test11.csv +0 -0
  20. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/eda/__init__.py +0 -0
  21. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/eda/plots.py +0 -0
  22. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/factor_analyzer/__init__.py +0 -0
  23. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
  24. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
  25. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
  26. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
  27. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/__init__.py +0 -0
  28. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/cd_data.csv +0 -0
  29. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/forr08a.py +0 -0
  30. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/mo.py +0 -0
  31. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/remote.py +0 -0
  32. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/so.py +0 -0
  33. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/function/torch_objective.py +0 -0
  34. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/hyperparameters/__init__.py +0 -0
  35. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/hyperparameters/parameters.py +0 -0
  36. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
  37. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/inspection/__init__.py +0 -0
  38. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/inspection/importance.py +0 -0
  39. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/inspection/predictions.py +0 -0
  40. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/mo/__init__.py +0 -0
  41. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/mo/mo_mm.py +0 -0
  42. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/mo/pareto.py +0 -0
  43. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/nn/__init__.py +0 -0
  44. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/nn/linear_regressor.py +0 -0
  45. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/nn/mlp.py +0 -0
  46. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/optimizer/__init__.py +0 -0
  47. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/optimizer/schedule_free.py +0 -0
  48. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/optimizer/wrapper.py +0 -0
  49. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/plot/__init__.py +0 -0
  50. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/plot/contour.py +0 -0
  51. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/plot/mo.py +0 -0
  52. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/plot/visualization.py +0 -0
  53. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/py.typed +0 -0
  54. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/reporting/__init__.py +0 -0
  55. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/reporting/analysis.py +0 -0
  56. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/reporting/results.py +0 -0
  57. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/sampling/__init__.py +0 -0
  58. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/sampling/design.py +0 -0
  59. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/sampling/effects.py +0 -0
  60. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/sampling/lhs.py +0 -0
  61. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/sampling/mm.py +0 -0
  62. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/__init__.py +0 -0
  63. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/kernels.py +0 -0
  64. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/kriging.py +0 -0
  65. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
  66. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/nystroem.py +0 -0
  67. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/pipeline.py +0 -0
  68. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/surrogate/simple_kriging.py +0 -0
  69. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/tricands/__init__.py +0 -0
  70. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/tricands/tricands.py +0 -0
  71. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/__init__.py +0 -0
  72. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/boundaries.py +0 -0
  73. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/convert.py +0 -0
  74. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/dimreduction.py +0 -0
  75. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/eval.py +0 -0
  76. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/file.py +0 -0
  77. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/mapping.py +0 -0
  78. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/ocba.py +0 -0
  79. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/pca.py +0 -0
  80. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/scaler.py +0 -0
  81. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/serialization.py +0 -0
  82. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/stats.py +0 -0
  83. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/tensorboard.py +0 -0
  84. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/transform.py +0 -0
  85. {spotoptim-2.1.0 → spotoptim-2.1.2}/src/spotoptim/utils/variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spotoptim
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Sequential Parameter Optimization Toolbox
5
5
  Author: bartzbeielstein
6
6
  Author-email: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com>
@@ -8,11 +8,11 @@ License: AGPL-3.0-or-later
8
8
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
9
9
  Classifier: Programming Language :: Python :: 3.13
10
10
  Classifier: Programming Language :: Python :: 3.14
11
- Requires-Dist: numpy>=1.24.3
12
- Requires-Dist: scipy>=1.10.1
13
- Requires-Dist: scikit-learn>=1.5.0
14
- Requires-Dist: pandas>=2.1.0
15
- Requires-Dist: tabulate>=0.9.0
11
+ Requires-Dist: numpy>=2.4
12
+ Requires-Dist: scipy>=1.18
13
+ Requires-Dist: scikit-learn>=1.8
14
+ Requires-Dist: pandas>=3.0
15
+ Requires-Dist: tabulate>=0.10
16
16
  Requires-Dist: dill>=0.4.1
17
17
  Requires-Dist: spotoptim[torch,viz,stats,remote] ; extra == 'all'
18
18
  Requires-Dist: pytest>=7.4.0 ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spotoptim"
3
- version = "2.1.0"
3
+ version = "2.1.2"
4
4
  description = "Sequential Parameter Optimization Toolbox"
5
5
  readme = "README.md"
6
6
  license = { text = "AGPL-3.0-or-later" }
@@ -14,11 +14,11 @@ authors = [
14
14
  ]
15
15
  requires-python = ">=3.13"
16
16
  dependencies = [
17
- "numpy>=1.24.3",
18
- "scipy>=1.10.1",
19
- "scikit-learn>=1.5.0",
20
- "pandas>=2.1.0",
21
- "tabulate>=0.9.0",
17
+ "numpy>=2.4",
18
+ "scipy>=1.18",
19
+ "scikit-learn>=1.8",
20
+ "pandas>=3.0",
21
+ "tabulate>=0.10",
22
22
  "dill>=0.4.1",
23
23
  ]
24
24
 
@@ -117,6 +117,107 @@ def prepare_de_kwargs(optimizer: SpotOptimProtocol, x0=None):
117
117
  return filtered_kwargs
118
118
 
119
119
 
120
+ # Normalized-space margin used to pull a warm-start x0 strictly inside the unit
121
+ # interval that scipy's DifferentialEvolutionSolver validates against. It must
122
+ # exceed the ~1e-16 rounding granularity of that rescaling by several orders of
123
+ # magnitude (robust) while staying negligible in parameter space (faithful).
124
+ _DE_X0_NORM_MARGIN = 1e-12
125
+
126
+
127
+ def sanitize_de_x0(
128
+ x0: np.ndarray, bounds: Optional[List[Tuple[float, float]]]
129
+ ) -> Optional[np.ndarray]:
130
+ """Make a warm-start ``x0`` acceptable to scipy ``differential_evolution``.
131
+
132
+ ``DifferentialEvolutionSolver`` does not validate ``x0`` against the raw box
133
+ ``[lower, upper]``. It rescales every coordinate into a unit interval using
134
+ ``scaled = (x0 - mid) * recip + 0.5`` with ``mid = (lower + upper) / 2`` and
135
+ ``recip = 1 / |upper - lower|`` (``0`` when ``lower == upper``), then rejects
136
+ the whole run if any ``scaled`` falls outside ``[0, 1]``.
137
+
138
+ An incumbent that legitimately sits *on* a box bound (optimizers converge to
139
+ corners; clipped warm-starts and estimator defaults land exactly on a bound)
140
+ can rescale to a hair outside ``[0, 1]`` purely from floating-point rounding
141
+ — e.g. ``0.01`` on the bound ``(0.01, 100.0)`` normalizes to ``-1.1e-16``.
142
+ A raw-space nudge such as ``np.nextafter`` cannot repair this: one ULP at a
143
+ small bound (``~1.7e-18`` at ``0.01``) is far below the rounding granularity
144
+ of the rescaling at the span's magnitude, so the nudged value normalizes to
145
+ the same out-of-range number. The nudge is simply in the wrong metric.
146
+
147
+ This helper therefore clamps in scipy's *own* normalized metric and maps the
148
+ result back with scipy's exact inverse, leaving an already-valid ``x0``
149
+ untouched (bit-for-bit) and returning ``None`` only if a coordinate cannot be
150
+ made interior — in which case the caller drops the optional warm-start.
151
+
152
+ Args:
153
+ x0 (ndarray): Candidate warm-start point in internal (transformed) scale,
154
+ shape ``(n_features,)``.
155
+ bounds (list of tuple): Per-dimension ``(lower, upper)`` bounds in
156
+ internal scale, exactly as passed to ``differential_evolution``.
157
+
158
+ Returns:
159
+ Optional[ndarray]: ``x0`` unchanged when scipy already accepts it; a
160
+ minimally adjusted copy guaranteed to pass scipy's check; or ``None``
161
+ when no acceptable point exists — ``bounds`` is ``None``, ``x0`` is not
162
+ all finite, or a coordinate cannot be made interior. ``None`` simply
163
+ drops the optional warm-start for that infill.
164
+
165
+ Examples:
166
+ ```{python}
167
+ import numpy as np
168
+ from spotoptim.optimizer.acquisition import sanitize_de_x0
169
+
170
+ # An incumbent exactly on the lower bound of a wide span: the raw value
171
+ # normalizes just below 0 and scipy would reject it.
172
+ bounds = [(0.01, 100.0)]
173
+ x0 = np.array([0.01])
174
+ fixed = sanitize_de_x0(x0, bounds)
175
+ lb, ub = 0.01, 100.0
176
+ mid, span = 0.5 * (lb + ub), abs(ub - lb)
177
+ scaled = (fixed[0] - mid) / span + 0.5
178
+ print(f"accepted by scipy: {0.0 <= scaled <= 1.0}")
179
+ print(f"distance from incumbent: {fixed[0] - 0.01:.2e}")
180
+ ```
181
+ """
182
+ if bounds is None:
183
+ return None
184
+ x0 = np.asarray(x0, dtype=float).ravel()
185
+ # A non-finite incumbent is pathological; never use it as a warm-start.
186
+ if not np.all(np.isfinite(x0)):
187
+ return None
188
+ lower = np.array([b[0] for b in bounds], dtype=float)
189
+ upper = np.array([b[1] for b in bounds], dtype=float)
190
+
191
+ # scipy's exact rescaling constants (see DifferentialEvolutionSolver.__init__
192
+ # and _unscale_parameters): note span uses |upper - lower|, so reversed
193
+ # bounds are handled identically to scipy.
194
+ mid = 0.5 * (lower + upper)
195
+ span = np.abs(lower - upper)
196
+ with np.errstate(divide="ignore", invalid="ignore"):
197
+ recip = np.where(span > 0.0, 1.0 / span, 0.0)
198
+
199
+ scaled = (x0 - mid) * recip + 0.5
200
+
201
+ # Fast path: scipy already accepts every coordinate -> leave x0 untouched so
202
+ # currently-working runs stay bit-for-bit identical.
203
+ if not np.any((scaled < 0.0) | (scaled > 1.0)):
204
+ return x0
205
+
206
+ # Clamp in scipy's normalized metric, then map back with its exact inverse
207
+ # (_scale_parameters: x = mid + (scaled - 0.5) * span).
208
+ scaled = np.clip(scaled, _DE_X0_NORM_MARGIN, 1.0 - _DE_X0_NORM_MARGIN)
209
+ x0_fixed = mid + (scaled - 0.5) * span
210
+
211
+ # Defense in depth: re-run scipy's exact check and drop x0 if any residual
212
+ # rounding still leaves a coordinate outside the unit interval. With finite
213
+ # x0 and finite bounds this is algebraically unreachable (the margin is far
214
+ # above the round-trip rounding error); it guards against future drift.
215
+ check = (x0_fixed - mid) * recip + 0.5
216
+ if not np.all(np.isfinite(x0_fixed)) or np.any((check < 0.0) | (check > 1.0)):
217
+ return None
218
+ return x0_fixed
219
+
220
+
120
221
  def optimize_acquisition_de(optimizer: SpotOptimProtocol) -> np.ndarray:
121
222
  """Optimize using differential evolution.
122
223
 
@@ -140,11 +241,17 @@ def optimize_acquisition_de(optimizer: SpotOptimProtocol) -> np.ndarray:
140
241
 
141
242
  if best_x is not None:
142
243
  best_x = optimizer.transform_X(best_x)
143
- # Nudge x0 strictly inside DE bounds using nextafter
144
- _lb = np.array([b[0] for b in optimizer.bounds])
145
- _ub = np.array([b[1] for b in optimizer.bounds])
146
- best_x = np.clip(best_x, np.nextafter(_lb, _ub), np.nextafter(_ub, _lb))
147
- best_X = best_x if optimizer.rng.rand() < optimizer.de_x0_prob else None
244
+ # Make x0 acceptable to scipy's differential_evolution, which validates
245
+ # x0 in a NORMALIZED metric rather than the raw box. An incumbent on a
246
+ # box bound can otherwise be rejected by floating-point rounding; a
247
+ # raw-space nudge (np.nextafter) is in the wrong metric and too small to
248
+ # survive scipy's rescaling when |bound| << span (see sanitize_de_x0).
249
+ best_x = sanitize_de_x0(best_x, optimizer.bounds)
250
+ # Draw unconditionally within this branch so the RNG stream is identical
251
+ # whether or not sanitization succeeds; drop the optional warm-start if
252
+ # it could not be sanitized.
253
+ use_x0 = optimizer.rng.rand() < optimizer.de_x0_prob
254
+ best_X = best_x if (best_x is not None and use_x0) else None
148
255
  else:
149
256
  best_X = None
150
257
 
File without changes